Skip to content

Commit 6e7cc78

Browse files
authored
Merge pull request #189 from Josue19-08/feat/recovery-system
Implement backup and recovery system
2 parents cadf9ca + 226cc90 commit 6e7cc78

File tree

16 files changed

+669
-704
lines changed

16 files changed

+669
-704
lines changed
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2025 SkillCert
3+
4+
use crate::schema::{Course, CourseBackupData, CourseCategory, CourseGoal, CourseId, CourseModule, DataKey};
5+
use soroban_sdk::{Address, Env, Map, String, Vec};
6+
7+
/// Export all course data for backup purposes
8+
///
9+
/// This function creates a complete backup of all course data including courses,
10+
/// categories, modules, goals, and prerequisites.
11+
///
12+
/// # Arguments
13+
/// * `env` - Soroban environment
14+
/// * `caller` - Address requesting the backup (must be admin)
15+
///
16+
/// # Returns
17+
/// * `CourseBackupData` - Complete backup structure
18+
///
19+
/// # Panics
20+
/// * If caller is not an admin
21+
pub fn export_course_data(env: Env, caller: Address) -> CourseBackupData {
22+
caller.require_auth();
23+
24+
// Verify caller is admin
25+
if !is_admin(&env, caller) {
26+
panic!("Unauthorized: Only admins can export course data");
27+
}
28+
29+
// Initialize maps for backup data
30+
let mut courses = Map::new(&env);
31+
let mut categories = Map::new(&env);
32+
let mut modules = Map::new(&env);
33+
let mut goals = Map::new(&env);
34+
let mut prerequisites = Map::new(&env);
35+
36+
// Get all courses by iterating through course IDs
37+
// Courses are stored as (Symbol("course"), course_id) -> Course
38+
let course_key = soroban_sdk::symbol_short!("course");
39+
let mut all_courses = Vec::new(&env);
40+
41+
// Get the current course ID to know how many courses exist
42+
let course_id_key = soroban_sdk::symbol_short!("course");
43+
let max_course_id: u128 = env
44+
.storage()
45+
.persistent()
46+
.get(&course_id_key)
47+
.unwrap_or(0u128);
48+
49+
// Iterate through all possible course IDs
50+
for id in 1..=max_course_id {
51+
let course_id_str = super::utils::u32_to_string(&env, id as u32);
52+
let storage_key = (course_key.clone(), course_id_str.clone());
53+
54+
if let Some(course) = env.storage().persistent().get::<_, Course>(&storage_key) {
55+
all_courses.push_back(course.clone());
56+
courses.set(course.id.clone(), course.clone());
57+
58+
// Export course goals
59+
if let Some(course_goals) = env
60+
.storage()
61+
.persistent()
62+
.get::<DataKey, Vec<CourseGoal>>(&DataKey::CourseGoalList(course.id.clone()))
63+
{
64+
goals.set(course.id.clone(), course_goals);
65+
}
66+
67+
// Export course prerequisites
68+
if let Some(course_prereqs) = env
69+
.storage()
70+
.persistent()
71+
.get::<DataKey, Vec<CourseId>>(&DataKey::CoursePrerequisites(course.id.clone()))
72+
{
73+
prerequisites.set(course.id.clone(), course_prereqs);
74+
}
75+
76+
// Export course modules (simplified version)
77+
let module_id = String::from_str(&env, "default_module");
78+
let course_module = CourseModule {
79+
id: module_id.clone(),
80+
course_id: course.id.clone(),
81+
position: 1,
82+
title: String::from_str(&env, "Default Module"),
83+
created_at: env.ledger().timestamp(),
84+
};
85+
modules.set(module_id, course_module);
86+
}
87+
}
88+
89+
90+
// Export all categories
91+
let mut category_id = 1u128;
92+
loop {
93+
if let Some(category) = env
94+
.storage()
95+
.persistent()
96+
.get::<DataKey, CourseCategory>(&DataKey::CourseCategory(category_id))
97+
{
98+
categories.set(category_id, category);
99+
category_id += 1;
100+
} else {
101+
break;
102+
}
103+
104+
// Safety check to avoid infinite loops
105+
if category_id > 10000 {
106+
break;
107+
}
108+
}
109+
110+
// Get category sequence counter
111+
let category_seq: u128 = env
112+
.storage()
113+
.persistent()
114+
.get(&DataKey::CategorySeq)
115+
.unwrap_or(0);
116+
117+
// Get admin list
118+
let admins: Vec<Address> = env
119+
.storage()
120+
.persistent()
121+
.get(&DataKey::Admins)
122+
.unwrap_or(Vec::new(&env));
123+
124+
// Create backup data structure
125+
CourseBackupData {
126+
courses,
127+
categories,
128+
modules,
129+
goals,
130+
prerequisites,
131+
category_seq,
132+
admins,
133+
backup_timestamp: env.ledger().timestamp(),
134+
backup_version: String::from_str(&env, "1.0.0"),
135+
}
136+
}
137+
138+
/// Import course data from backup
139+
///
140+
/// This function restores course data from a backup structure.
141+
/// This operation will overwrite existing data.
142+
///
143+
/// # Arguments
144+
/// * `env` - Soroban environment
145+
/// * `caller` - Address performing the import (must be admin)
146+
/// * `backup_data` - Backup data to restore
147+
///
148+
/// # Returns
149+
/// * `u32` - Number of courses imported
150+
///
151+
/// # Panics
152+
/// * If caller is not an admin
153+
/// * If backup data is invalid
154+
pub fn import_course_data(env: Env, caller: Address, backup_data: CourseBackupData) -> u32 {
155+
caller.require_auth();
156+
157+
// Verify caller is admin
158+
if !is_admin(&env, caller) {
159+
panic!("Unauthorized: Only admins can import course data");
160+
}
161+
162+
// Validate backup version compatibility
163+
let expected_version = String::from_str(&env, "1.0.0");
164+
if backup_data.backup_version != expected_version {
165+
panic!("Incompatible backup version");
166+
}
167+
168+
let mut imported_count = 0u32;
169+
let course_key = soroban_sdk::symbol_short!("course");
170+
171+
// Import courses - store each course individually
172+
for (_course_id, course) in backup_data.courses.iter() {
173+
let storage_key = (course_key.clone(), course.id.clone());
174+
env.storage()
175+
.persistent()
176+
.set(&storage_key, &course);
177+
imported_count += 1;
178+
}
179+
180+
// Import categories
181+
for (category_id, category) in backup_data.categories.iter() {
182+
env.storage()
183+
.persistent()
184+
.set(&DataKey::CourseCategory(category_id), &category);
185+
}
186+
187+
// Import modules
188+
for (module_id, module) in backup_data.modules.iter() {
189+
env.storage()
190+
.persistent()
191+
.set(&DataKey::Module(module_id), &module);
192+
}
193+
194+
// Import goals
195+
for (course_id, course_goals) in backup_data.goals.iter() {
196+
env.storage()
197+
.persistent()
198+
.set(&DataKey::CourseGoalList(course_id), &course_goals);
199+
}
200+
201+
// Import prerequisites
202+
for (course_id, prereqs) in backup_data.prerequisites.iter() {
203+
env.storage()
204+
.persistent()
205+
.set(&DataKey::CoursePrerequisites(course_id), &prereqs);
206+
}
207+
208+
// Import category sequence counter
209+
env.storage()
210+
.persistent()
211+
.set(&DataKey::CategorySeq, &backup_data.category_seq);
212+
213+
// Import admin list
214+
env.storage()
215+
.persistent()
216+
.set(&DataKey::Admins, &backup_data.admins);
217+
218+
// Emit import event
219+
env.events().publish(
220+
(String::from_str(&env, "course_data_imported"),),
221+
(imported_count, backup_data.backup_timestamp),
222+
);
223+
224+
imported_count
225+
}
226+
227+
/// Check if an address is an admin
228+
///
229+
/// This is a simplified version for the backup system.
230+
/// In a real implementation, this would check against the user_management contract.
231+
fn is_admin(env: &Env, address: Address) -> bool {
232+
let admins: Vec<Address> = env
233+
.storage()
234+
.persistent()
235+
.get(&DataKey::Admins)
236+
.unwrap_or(Vec::new(env));
237+
238+
for admin in admins.iter() {
239+
if admin == address {
240+
return true;
241+
}
242+
}
243+
false
244+
}

contracts/course/course_registry/src/functions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod access_control;
55
pub mod add_goal;
66
pub mod add_module;
77
pub mod archive_course;
8+
pub mod backup_recovery;
89
pub mod contract_versioning;
910
pub mod create_course;
1011
pub mod create_course_category;

contracts/course/course_registry/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,45 @@ impl CourseRegistry {
930930
)
931931
}
932932

933+
/// Export all course data for backup purposes (admin only)
934+
///
935+
/// This function exports all course data including courses, categories,
936+
/// modules, goals, and prerequisites for backup and recovery purposes.
937+
///
938+
/// # Arguments
939+
/// * `env` - Soroban environment
940+
/// * `caller` - Address performing the export (must be admin)
941+
///
942+
/// # Returns
943+
/// * `CourseBackupData` - Complete backup data structure
944+
///
945+
/// # Panics
946+
/// * If caller is not an admin
947+
pub fn export_course_data(env: Env, caller: Address) -> crate::schema::CourseBackupData {
948+
functions::backup_recovery::export_course_data(env, caller)
949+
}
950+
951+
/// Import course data from backup (admin only)
952+
///
953+
/// This function imports course data from a backup structure.
954+
/// Only admins can perform this operation. This will overwrite existing data.
955+
///
956+
/// # Arguments
957+
/// * `env` - Soroban environment
958+
/// * `caller` - Address performing the import (must be admin)
959+
/// * `backup_data` - Backup data structure to import
960+
///
961+
/// # Returns
962+
/// * `u32` - Number of courses imported
963+
///
964+
/// # Panics
965+
/// * If caller is not an admin
966+
/// * If backup data is invalid
967+
/// * If import operation fails
968+
pub fn import_course_data(env: Env, caller: Address, backup_data: crate::schema::CourseBackupData) -> u32 {
969+
functions::backup_recovery::import_course_data(env, caller, backup_data)
970+
}
971+
933972
/// Get the current contract version
934973
///
935974
/// Returns the semantic version of the current contract deployment.
@@ -1008,4 +1047,5 @@ impl CourseRegistry {
10081047
pub fn get_migration_status(env: Env) -> String {
10091048
functions::contract_versioning::get_migration_status(&env)
10101049
}
1050+
10111051
}

contracts/course/course_registry/src/schema.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,30 @@ pub struct EditCourseParams {
145145
pub new_level: Option<Option<CourseLevel>>,
146146
pub new_duration_hours: Option<Option<u32>>,
147147
}
148+
149+
/// Backup data structure for course registry system.
150+
///
151+
/// Contains all course data, categories, modules, goals, and prerequisites
152+
/// for backup and recovery operations.
153+
#[contracttype]
154+
#[derive(Clone, Debug, PartialEq)]
155+
pub struct CourseBackupData {
156+
/// All courses in the system
157+
pub courses: soroban_sdk::Map<String, Course>,
158+
/// All course categories
159+
pub categories: soroban_sdk::Map<u128, CourseCategory>,
160+
/// All course modules
161+
pub modules: soroban_sdk::Map<String, CourseModule>,
162+
/// All course goals mapped by (course_id, goal_id)
163+
pub goals: soroban_sdk::Map<String, soroban_sdk::Vec<CourseGoal>>,
164+
/// Course prerequisites mapping
165+
pub prerequisites: soroban_sdk::Map<String, soroban_sdk::Vec<CourseId>>,
166+
/// Category sequence counter
167+
pub category_seq: u128,
168+
/// List of admin addresses
169+
pub admins: soroban_sdk::Vec<Address>,
170+
/// Backup timestamp
171+
pub backup_timestamp: u64,
172+
/// Backup version for compatibility
173+
pub backup_version: String,
174+
}

0 commit comments

Comments
 (0)