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
114 changes: 114 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Guild Backend (Rust + Axum + SQLx)

This service exposes a REST API for managing profiles, backed by PostgreSQL.

- HTTP: 0.0.0.0:3001
- DB: PostgreSQL (SQLx)
- Migrations: SQLx migrator (on startup and via `bin/migrate`)

## 1) Prerequisites
- Rust (latest stable recommended)
- PostgreSQL 14+ (`initdb`, `pg_ctl`, `psql` available)

## 2) Environment
Create `backend/.env`:
```
DATABASE_URL=postgresql://guild_user:guild_password@localhost:5432/guild_genesis
RUST_LOG=guild_backend=debug,tower_http=debug
```
The server requires `DATABASE_URL` at runtime.

## 3) Start local Postgres
From the repo root, using Just:
```
just db-setup
```
This will:
- init `.postgres/` if missing
- start Postgres on localhost:5432
- create DB `guild_genesis` and user `guild_user/guild_password`
- run backend migrations

Manual alternative (repo root):
```
initdb -D .postgres
pg_ctl -D .postgres -l .postgres/postgres.log start

createdb guild_genesis || true
psql -d guild_genesis -c "CREATE USER guild_user WITH PASSWORD 'guild_password';" || true
psql -d guild_genesis -c "GRANT ALL PRIVILEGES ON DATABASE guild_genesis TO guild_user;" || true
```
Stop Postgres:
```
pg_ctl -D .postgres stop
```

## 4) Run migrations (optional)
Migrations run on server startup. To run explicitly:
```
cd backend
cargo run --bin guild-backend
```

## 5) Launch the API
```
cd backend
cargo run
```
The server listens on `http://0.0.0.0:3001`.

## 6) API quickstart
All endpoints require Ethereum header-based auth.

Create profile:
```
curl -X POST \
-H 'Content-Type: application/json' \
-H 'x-eth-address: 0x2581aAa94299787a8A588B2Fceb161A302939E28' \
-H 'x-eth-signature: 0x00000000000000' \
-H 'x-siwe-message: LOGIN_NONCE' \
-d '{
"name": "My profile",
"description": "Hello world",
"avatar_url": "https://example.com/avatar.png"
}' \
http://0.0.0.0:3001/profiles/
```
Get profile:
```
curl -H 'x-eth-address: 0x2581aAa94299787a8A588B2Fceb161A302939E28' \
-H 'x-eth-signature: 0x00000000000000' \
-H 'x-siwe-message: LOGIN_NONCE' \
http://0.0.0.0:3001/profiles/0x2581aAa94299787a8A588B2Fceb161A302939E28
```
Update profile:
```
curl -X PUT \
-H 'Content-Type: application/json' \
-H 'x-eth-address: 0x2581aAa94299787a8A588B2Fceb161A302939E28' \
-H 'x-eth-signature: 0x00000000000000' \
-H 'x-siwe-message: LOGIN_NONCE' \
-d '{ "name": "New name", "description": "New desc" }' \
http://0.0.0.0:3001/profiles/0x2581aAa94299787a8A588B2Fceb161A302939E28
```

## 7) Troubleshooting
- initdb locale error:
```
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 initdb --locale=en_US.UTF-8 --encoding=UTF8 -D .postgres
```
- Permission denied on schema `public`:
```
psql -U postgres -d guild_genesis -c "ALTER SCHEMA public OWNER TO guild_user;"
psql -U postgres -d guild_genesis -c "GRANT USAGE, CREATE ON SCHEMA public TO guild_user;"
```
- Rust edition 2024 error: repo pins `base64ct = 1.7.3`. If still present, `rustup update` or `rustup override set nightly` in `backend/`.

## 8) Structure
- `src/main.rs`: boot server, run migrations
- `src/bin/migrate.rs`: standalone migrator
- `src/presentation`: routes, handlers, middlewares
- `src/infrastructure`: Postgres repository, Ethereum verification
- `src/domain`: entities, repository traits, services
- `src/application`: commands and DTOs
- `migrations/`: SQLx migrations
24 changes: 24 additions & 0 deletions backend/src/application/commands/get_all_profiles.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use crate::application::dtos::profile_dtos::ProfileResponse;
use crate::domain::repositories::profile_repository::ProfileRepository;
use std::sync::Arc;

pub async fn get_all_profiles(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍

profile_repository: Arc<dyn ProfileRepository + 'static>,
) -> Result<Vec<ProfileResponse>, String> {
let profiles = profile_repository
.find_all()
.await
.map_err(|e| e.to_string())?;

Ok(profiles
.into_iter()
.map(|profile| ProfileResponse {
address: profile.address,
name: profile.name.unwrap_or_default(),
description: profile.description,
avatar_url: profile.avatar_url,
created_at: profile.created_at,
updated_at: profile.updated_at,
})
.collect())
}
1 change: 1 addition & 0 deletions backend/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod create_profile;
pub mod get_all_profiles;
pub mod get_profile;
pub mod update_profile;
1 change: 1 addition & 0 deletions backend/src/domain/repositories/profile_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub trait ProfileRepository: Send + Sync {
&self,
address: &WalletAddress,
) -> Result<Option<Profile>, Box<dyn std::error::Error>>;
async fn find_all(&self) -> Result<Vec<Profile>, Box<dyn std::error::Error>>;
async fn create(&self, profile: &Profile) -> Result<(), Box<dyn std::error::Error>>;
async fn update(&self, profile: &Profile) -> Result<(), Box<dyn std::error::Error>>;
async fn delete(&self, address: &WalletAddress) -> Result<(), Box<dyn std::error::Error>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ impl ProfileRepository for PostgresProfileRepository {
}))
}

async fn find_all(&self) -> Result<Vec<Profile>, Box<dyn std::error::Error>> {
let rows = sqlx::query!(
r#"
SELECT address, name, description, avatar_url, created_at, updated_at
FROM profiles
"#,
)
.fetch_all(&self.pool)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

Ok(rows
.into_iter()
.map(|r| Profile {
address: WalletAddress(r.address),
name: r.name,
description: r.description,
avatar_url: r.avatar_url,
created_at: r.created_at.unwrap(),
updated_at: r.updated_at.unwrap(),
})
.collect())
}

async fn create(&self, profile: &Profile) -> Result<(), Box<dyn std::error::Error>> {
sqlx::query!(
r#"
Expand Down
4 changes: 2 additions & 2 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub mod presentation;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load env before reading variables
dotenvy::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

tracing_subscriber::registry()
Expand All @@ -19,8 +21,6 @@ async fn main() -> anyhow::Result<()> {
.with(tracing_subscriber::fmt::layer())
.init();

dotenvy::dotenv().ok();

let pool = sqlx::PgPool::connect(&database_url)
.await
.unwrap_or_else(|_| {
Expand Down
18 changes: 13 additions & 5 deletions backend/src/presentation/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ use tower_http::{
};

use super::handlers::{
create_profile_handler, delete_profile_handler, get_profile_handler, update_profile_handler,
create_profile_handler, delete_profile_handler, get_all_profiles_handler, get_profile_handler,
update_profile_handler,
};

use super::middlewares::eth_auth_layer;

pub async fn create_app(pool: sqlx::PgPool) -> Router {
Expand All @@ -35,12 +37,19 @@ pub async fn create_app(pool: sqlx::PgPool) -> Router {

let protected = Router::new()
.route("/profiles/", post(create_profile_handler))
.route("/profiles/:address", get(get_profile_handler))
.route("/profiles/:address", put(update_profile_handler))
.route("/profiles/:address", delete(delete_profile_handler));
.route("/profiles/:address", delete(delete_profile_handler))
.with_state(state.clone())
.layer(from_fn_with_state(state.clone(), eth_auth_layer));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


let public = Router::new()
.route("/profiles/:address", get(get_profile_handler))
.route("/profiles/", get(get_all_profiles_handler))
.with_state(state.clone());

Router::new()
.nest("/", protected)
.merge(public)
.with_state(state.clone())
.layer(
ServiceBuilder::new()
Expand All @@ -51,8 +60,7 @@ pub async fn create_app(pool: sqlx::PgPool) -> Router {
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers(Any),
)
.layer(DefaultBodyLimit::max(1024 * 1024))
.layer(from_fn_with_state(state, eth_auth_layer)),
.layer(DefaultBodyLimit::max(1024 * 1024)),
)
}

Expand Down
8 changes: 6 additions & 2 deletions backend/src/presentation/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use axum::{extract::State, http::StatusCode, Extension, Json};
use crate::{
application::{
commands::{
create_profile::create_profile, get_profile::get_profile,
update_profile::update_profile,
create_profile::create_profile, get_all_profiles::get_all_profiles,
get_profile::get_profile, update_profile::update_profile,
},
dtos::{CreateProfileRequest, ProfileResponse, UpdateProfileRequest},
},
Expand All @@ -31,6 +31,10 @@ pub async fn get_profile_handler(
Json(get_profile(state.profile_repository, wallet).await.unwrap())
}

pub async fn get_all_profiles_handler(State(state): State<AppState>) -> Json<Vec<ProfileResponse>> {
Json(get_all_profiles(state.profile_repository).await.unwrap())
}

pub async fn update_profile_handler(
State(state): State<AppState>,
Extension(VerifiedWallet(wallet)): Extension<VerifiedWallet>,
Expand Down
2 changes: 1 addition & 1 deletion frontend/.astro/data-store.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.13.7","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false},\"legacy\":{\"collections\":false}}"]
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.13.7","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/node\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/Users/antoineestienne/GithubRepositories/TheGuildGenesis/frontend/node_modules/.astro/sessions\"}}}"]
1 change: 1 addition & 0 deletions frontend/.astro/types.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />
8 changes: 7 additions & 1 deletion frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
PUBLIC_WALLET_CONNECT_PROJECT_ID=your_project_id_here

# Backend API URL
PUBLIC_API_URL=http://localhost:3001
PUBLIC_API_URL=http://localhost:3001

# Contract addresses (values for amoy)
PUBLIC_BADGE_REGISTRY_ADDRESS=0x7e67100ce4bc2640f50c47d2dd3eebc749d8f52e
PUBLIC_EAS_CONTRACT_ADDRESS=0xb101275a60d8bfb14529C421899aD7CA1Ae5B5Fc
PUBLIC_ACTIVITY_TOKEN_ADDRESS=0x5f0a5293e33af3806ed34ba7dc139c8d3c39f310
PUBLIC_SCHEMA_ID=0x7b0ac75049ac0cf0a8f6606194f9ff2b892bed81560a7d84d484f96c788042cc
3 changes: 3 additions & 0 deletions frontend/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { defineConfig } from "astro/config";

import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
import node from "@astrojs/node";

// https://astro.build/config
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [react()],

vite: {
Expand Down
Loading
Loading