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
10 changes: 4 additions & 6 deletions contracts/course/course_access/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@ use soroban_sdk::{contracterror, panic_with_error, Env};
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
UserAlreadyHasAccess = 1,
UserNoAccessCourse = 2,
UserAlreadyHasAccess = 1,
UserNoAccessCourse = 2,
Unauthorized = 3,
NameRequired = 4,
EmailRequired = 5,
CountryRequired = 6,
InvalidCourseId = 7,
InvalidUser = 8,
EmptyCourseId = 9,
InvalidTransferData = 10,
SameUserTransfer = 11,
Initialized = 12
Initialized = 12,
OffChainRefIdRequired = 13,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down
37 changes: 16 additions & 21 deletions contracts/course/course_access/src/functions/save_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,34 @@ use crate::schema::{DataKey, UserProfile};
const SAVE_USER_PROFILE_EVENT: Symbol = symbol_short!("saveUsPrl");


/// Save a minimal on-chain user profile.
///
/// Stores only the user's address and an off-chain reference ID.
/// All PII (name, email, profession, goals, country) is stored off-chain.
///
/// # Arguments
/// * `env` - Soroban environment
/// * `off_chain_ref_id` - UUID/hash mapping to the user's full record in the off-chain DB
/// * `user` - The user's blockchain address
pub fn save_user_profile(
env: Env,
name: String,
email: String,
profession: Option<String>,
goals: Option<String>,
country: String,
off_chain_ref_id: String,
user: Address,
) {
// Validate required fields
if name.is_empty() {
handle_error(&env, Error::NameRequired)
}
// TODO: Implement full email validation according to RFC 5322 standard
if email.is_empty() {
handle_error(&env, Error::EmailRequired)
}
if country.is_empty() {
handle_error(&env, Error::CountryRequired)
// Validate required field
if off_chain_ref_id.is_empty() {
handle_error(&env, Error::OffChainRefIdRequired)
}

let profile: UserProfile = UserProfile {
name: name.clone(),
email: email.clone(),
profession: profession.clone(),
goals: goals.clone(),
country: country.clone(),
user: user.clone(),
off_chain_ref_id: off_chain_ref_id.clone(),
};

env.storage()
.persistent()
.set(&DataKey::UserProfile(user.clone()), &profile);

env.events()
.publish((SAVE_USER_PROFILE_EVENT,), (name, email, profession, goals, country, user));
.publish((SAVE_USER_PROFILE_EVENT,), (user, off_chain_ref_id));
}
60 changes: 10 additions & 50 deletions contracts/course/course_access/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ mod test;

use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec};

use functions::{config::initialize,config::set_contract_addrs, grant_access::course_access_grant_access, revoke_access::course_access_revoke_access, revoke_all_access::revoke_all_access, save_profile::save_user_profile, list_user_courses::list_user_courses, list_course_access::course_access_list_course_access, contract_versioning::{is_version_compatible, get_migration_status, get_version_history, migrate_access_data}, transfer_course_access::transfer_course_access};
use functions::{config::initialize, config::set_contract_addrs, grant_access::course_access_grant_access, revoke_access::course_access_revoke_access, revoke_all_access::revoke_all_access, save_profile::save_user_profile, list_user_courses::list_user_courses, list_course_access::course_access_list_course_access, contract_versioning::{is_version_compatible, get_migration_status, get_version_history, migrate_access_data}, transfer_course_access::transfer_course_access};
use schema::{CourseUsers, UserCourses};

/// Course Access Contract
///
/// This contract manages user access to courses in the SkillCert platform.
/// It provides functionality to grant, revoke, and query course access permissions,
/// as well as manage user profiles.
/// as well as store minimal on-chain user profiles (address + off-chain ref ID only).
#[contract]
pub struct CourseAccessContract;

Expand Down Expand Up @@ -163,65 +163,25 @@ impl CourseAccessContract {
course_access_revoke_access(env, course_id, user)
}

/// Save or update a user's profile on-chain.
/// Save or update a minimal on-chain user profile.
///
/// Stores user profile information in the contract storage.
/// This includes personal and professional information.
/// Stores only the user's address and an off-chain reference ID.
/// All PII (name, email, profession, goals, country) is stored off-chain.
///
/// # Arguments
///
/// * `env` - The Soroban environment
/// * `name` - The user's full name
/// * `email` - The user's email address
/// * `profession` - Optional profession/job title
/// * `goals` - Optional learning goals or objectives
/// * `country` - The user's country of residence
/// * `off_chain_ref_id` - UUID/hash mapping to the user's full record in the off-chain DB
///
/// # Panics
///
/// * If name, email, or country are empty
/// * If email format is invalid
///
/// # Examples
///
/// ```rust
/// // Save user profile
/// contract.save_user_profile(
/// env.clone(),
/// "John Doe".try_into().unwrap(),
/// "john@example.com".try_into().unwrap(),
/// Some("Software Developer".try_into().unwrap()),
/// Some("Learn Rust programming".try_into().unwrap()),
/// "US".try_into().unwrap()
/// );
///
/// // Save minimal profile
/// contract.save_user_profile(
/// env.clone(),
/// "Jane Smith".try_into().unwrap(),
/// "jane@example.com".try_into().unwrap(),
/// None,
/// None,
/// "CA".try_into().unwrap()
/// );
/// ```
///
/// # Edge Cases
///
/// * **Empty required fields**: Name, email, and country cannot be empty
/// * **Invalid email**: Email must be in valid format
/// * **Profile updates**: Overwrites existing profile data
/// * **Optional fields**: Profession and goals can be None
/// * If off_chain_ref_id is empty
pub fn save_user_profile(
env: Env,
name: String,
email: String,
profession: Option<String>,
goals: Option<String>,
country: String,
off_chain_ref_id: String,
) {
let user: Address = env.current_contract_address();
save_user_profile(env, name, email, profession, goals, country, user);
save_user_profile(env, off_chain_ref_id, user);
}

/// List all courses a user has access to.
Expand Down Expand Up @@ -463,7 +423,7 @@ impl CourseAccessContract {
get_migration_status(&env)
}

pub fn transfer_course(env: Env, course_id: String, from: Address, to: Address){
pub fn transfer_course(env: Env, course_id: String, from: Address, to: Address) {
transfer_course_access(env, course_id, from, to)
}
}
19 changes: 6 additions & 13 deletions contracts/course/course_access/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,16 @@ pub enum DataKey {
CourseUsers(String),
}

/// Represents a user's profile information.
/// on-chain user profile for the course_access contract.
///
/// This struct contains all the personal and professional information
/// that users can store on-chain as part of their profile.
/// Stores only the user's blockchain address and an off-chain reference ID.
#[derive(Clone, Debug, Eq, PartialEq)]
#[contracttype]
pub struct UserProfile {
/// The user's full name
pub name: String,
/// The user's email address
pub email: String,
/// Optional profession or job title
pub profession: Option<String>,
/// Optional learning goals or objectives
pub goals: Option<String>,
/// The user's country of residence
pub country: String,
/// The user's blockchain address
pub user: Address,
/// Off-chain reference ID (UUID mapping to DB record)
pub off_chain_ref_id: String,
}

/// Contains all users who have access to a specific course.
Expand Down
2 changes: 1 addition & 1 deletion contracts/course/course_access/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod user_management {
// For testing, always return true to simplify admin checks
true
}
pub fn save_user_profile(_env: Env, _user: Address, _name: String, _email: String) {
pub fn save_user_profile(_env: Env, _off_chain_ref_id: String) {
// Mock implementation
}
pub fn is_course_creator(_env: Env, _course_id: String, _user: Address) -> bool {
Expand Down
12 changes: 5 additions & 7 deletions contracts/course/course_registry/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ pub enum Error {
OnlyCreatorCanArchive = 4,
CourseAlreadyArchived = 5,
Unauthorized = 6,
/// Category name is required (used by create_course_category)
NameRequired = 7,
EmptyCourseTitle = 8,
InvalidPrice = 9,
DuplicateCourseTitle = 10,
DuplicateCourseId = 11,
OnlyCreatorCanEditPrereqs = 12,
PrereqCourseNotFound = 13,
Expand All @@ -31,18 +30,13 @@ pub enum Error {
UnauthorizedCaller = 401,
UnauthorizedCourseAccess = 402,
InvalidAdminOperation = 403,
EmptyModuleTitle = 404,
DuplicateModulePosition = 405,
EmptyModuleId = 22,
PrereqNotInList = 23,
InvalidModulePosition = 24,
InvalidModuleTitle = 25,
InvalidCourseDescription = 26,
InvalidCategoryName = 27,
EmptyCategory = 28,
InvalidTitleLength = 29,
InvalidLanguageLength = 43,
InvalidThumbnailUrlLength = 44,
InvalidDurationValue = 45,
InvalidLimitValue = 46,
InvalidOffsetValue = 47,
Expand All @@ -58,6 +52,10 @@ pub enum Error {
// Rate limiting errors
CourseRateLimitExceeded = 57,
CourseRateLimitNotConfigured = 58,
/// Content hash is required for on-chain integrity verification
ContentHashRequired = 59,
/// Off-chain reference ID is required
OffChainRefIdRequired = 60,
}

pub fn handle_error(env: &Env, error: Error) -> ! {
Expand Down
Loading