├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── deploy.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ansible ├── inventory.yml ├── playbook.yml └── roles │ ├── backend │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── container_analysis │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── frontend │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── infra_services │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── post_tasks │ └── tasks │ │ └── main.yml │ ├── prerequisites │ └── tasks │ │ └── main.yml │ ├── repo_sync │ └── tasks │ │ └── main.yml │ └── secrets │ ├── meta │ └── main.yml │ └── tasks │ └── main.yml ├── backend ├── Dockerfile ├── Dockerfile_celery ├── __init__.py ├── alembic.ini ├── app_state │ ├── __init__.py │ ├── bot.py │ ├── db.py │ ├── gcp.py │ ├── meilisearch.py │ ├── rbq.py │ └── redis.py ├── celery_app │ ├── __init__.py │ ├── celery_config.py │ └── tasks.py ├── common │ ├── cruds.py │ ├── dependencies.py │ ├── schemas.py │ └── utils │ │ ├── enums.py │ │ ├── meilisearch.py │ │ └── response_builder.py ├── core │ ├── configs │ │ ├── __init__.py │ │ ├── config.py │ │ └── coverpage.jpg │ └── database │ │ ├── __init__.py │ │ ├── manager.py │ │ └── models │ │ ├── __init__.py │ │ ├── base.py │ │ ├── common_enums.py │ │ ├── community.py │ │ ├── events.py │ │ ├── grade_report.py │ │ ├── media.py │ │ ├── notification.py │ │ ├── product.py │ │ ├── review.py │ │ ├── sgotinish.py │ │ └── user.py ├── lifespan.py ├── main.py ├── middlewares │ └── prometheus_metrics.py ├── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 0001_create_grade_reports_table.py │ │ ├── 1691549e89c9_transition_from_old_to_new_courses.py │ │ ├── 183fea04d6e8_added_verification_badge_to_communities.py │ │ ├── 18da0a04711e_set_default_false_on_communities_.py │ │ ├── 1ff9b347b5f3_initial_events_bootstrap.py │ │ ├── 2585ff06573e_added_templates.py │ │ ├── 2659177a3d3a_add_registration_link_to_events_table.py │ │ ├── 28748381dea2_added_sgotinish.py │ │ ├── 8299a73354e9_added_indexing.py │ │ ├── 93a473924e2c_added_gpa_calc_tables.py │ │ ├── bcb1ca0ddd00_add_recruitment_to_event_type_enum.py │ │ ├── c8dc2ede9974_merge_duplicate_student_courses.py │ │ ├── d0bf926cce2a_added_entity_type_tickets.py │ │ ├── de873a88b9db_add_end_datetime_to_events_table.py │ │ ├── df3b09aac8a7_adding_cps_school.py │ │ ├── fb04158b1dea_add_grades_to_entity_type_enum.py │ │ └── ff17af4961b9_obtained_max_course_scores.py ├── modules │ ├── __init__.py │ ├── auth │ │ ├── README.md │ │ ├── __init__.py │ │ ├── app_token.py │ │ ├── auth.py │ │ ├── cruds.py │ │ ├── dependencies.py │ │ ├── keycloak_manager.py │ │ ├── schemas.py │ │ └── utils.py │ ├── bot │ │ ├── README.md │ │ ├── bot.py │ │ ├── cruds.py │ │ ├── filters │ │ │ ├── __init__.py │ │ │ └── deeplink.py │ │ ├── hints_command.py │ │ ├── keyboards │ │ │ ├── callback_factory.py │ │ │ └── kb.py │ │ ├── locales │ │ │ ├── en │ │ │ │ └── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ └── messages.po │ │ │ ├── kz │ │ │ │ └── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ └── messages.po │ │ │ └── ru │ │ │ │ └── LC_MESSAGES │ │ │ │ ├── messages.mo │ │ │ │ └── messages.po │ │ ├── middlewares │ │ │ ├── __init__.py │ │ │ ├── bucket_client.py │ │ │ ├── db_session.py │ │ │ ├── i18n.py │ │ │ ├── public_url.py │ │ │ └── redis.py │ │ ├── routes │ │ │ ├── __init__.py │ │ │ └── user │ │ │ │ └── private │ │ │ │ ├── __init__.py │ │ │ │ ├── callback │ │ │ │ └── confirmation.py │ │ │ │ └── messages │ │ │ │ ├── start.py │ │ │ │ ├── start_deeplink.py │ │ │ │ └── student_validator.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── enums.py │ │ │ ├── google_bucket.py │ │ │ ├── permissions.py │ │ │ └── utils.py │ ├── campuscurrent │ │ ├── __init__.py │ │ ├── base.py │ │ ├── comments │ │ │ ├── __init__.py │ │ │ ├── comments.py │ │ │ ├── cruds.py │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ └── utils.py │ │ ├── communities │ │ │ ├── __init__.py │ │ │ ├── communities.py │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ └── utils.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── dependencies.py │ │ │ ├── events.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ ├── service.py │ │ │ └── utils.py │ │ ├── posts │ │ │ ├── __init__.py │ │ │ ├── cruds.py │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── posts.py │ │ │ ├── schemas.py │ │ │ └── utils.py │ │ ├── profile │ │ │ ├── __init__.py │ │ │ └── profile.py │ │ └── tags │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ └── tags.py │ ├── courses │ │ ├── crashed │ │ │ ├── crashed.py │ │ │ ├── dependencies.py │ │ │ ├── registrar │ │ │ │ ├── public_course_catalog.py │ │ │ │ ├── registrar_client.py │ │ │ │ └── registrar_parser.py │ │ │ ├── schemas.py │ │ │ └── service.py │ │ ├── csv_parsers │ │ │ ├── allcourses.csv │ │ │ ├── allcourses_parser.py │ │ │ ├── grade_reports.py │ │ │ ├── grades.csv │ │ │ ├── pcc_courses.csv │ │ │ ├── pcc_courses_parser.py │ │ │ └── pcc_search_dump.py │ │ ├── statistics │ │ │ ├── schemas.py │ │ │ └── statistics.py │ │ ├── student_courses │ │ │ ├── base.py │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ ├── service.py │ │ │ └── student_courses.py │ │ └── templates │ │ │ ├── dependencies.py │ │ │ ├── policy.py │ │ │ ├── schemas.py │ │ │ ├── service.py │ │ │ └── templates.py │ ├── google_bucket │ │ ├── __init__.py │ │ ├── cruds.py │ │ ├── dependencies.py │ │ ├── google_bucket.py │ │ ├── policy.py │ │ ├── schemas.py │ │ └── utils.py │ ├── kupiprodai │ │ ├── __init__.py │ │ ├── dependencies.py │ │ ├── policy.py │ │ ├── product.py │ │ ├── schemas.py │ │ └── utils.py │ ├── notification │ │ ├── notification.py │ │ ├── rate_limiter.py │ │ ├── schemas.py │ │ ├── service.py │ │ ├── tasks.py │ │ └── utils.py │ ├── review │ │ ├── dependencies.py │ │ ├── reply.py │ │ ├── review.py │ │ ├── schemas.py │ │ ├── service.py │ │ └── utils.py │ ├── search │ │ ├── __init__.py │ │ ├── search.py │ │ └── utils.py │ └── sgotinish │ │ ├── base.py │ │ ├── conversations │ │ ├── conversations.py │ │ ├── dependencies.py │ │ ├── policy.py │ │ ├── schemas.py │ │ └── service.py │ │ ├── messages │ │ ├── dependencies.py │ │ ├── messages.py │ │ ├── policy.py │ │ ├── schemas.py │ │ └── service.py │ │ └── tickets │ │ ├── cruds.py │ │ ├── delegation.py │ │ ├── dependencies.py │ │ ├── interfaces.py │ │ ├── policy.py │ │ ├── schemas.py │ │ ├── service.py │ │ └── tickets.py ├── poetry.lock ├── pyproject.toml ├── start.sh └── tests │ ├── __init__.py │ ├── conftest.py │ └── routes │ ├── __init__.py │ └── test_kupiprodai.py ├── docs ├── TODO.md └── wif-setup.md ├── frontend ├── Dockerfile_static_builder ├── Dockerfile_vite ├── index.html ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── App.tsx │ │ ├── index.css │ │ └── main.tsx │ ├── assets │ │ ├── favicon.png │ │ ├── icons │ │ │ ├── apple-icon-180.png │ │ │ ├── apple-splash-1125-2436.jpg │ │ │ ├── apple-splash-1136-640.jpg │ │ │ ├── apple-splash-1170-2532.jpg │ │ │ ├── apple-splash-1179-2556.jpg │ │ │ ├── apple-splash-1206-2622.jpg │ │ │ ├── apple-splash-1242-2208.jpg │ │ │ ├── apple-splash-1242-2688.jpg │ │ │ ├── apple-splash-1260-2736.jpg │ │ │ ├── apple-splash-1284-2778.jpg │ │ │ ├── apple-splash-1290-2796.jpg │ │ │ ├── apple-splash-1320-2868.jpg │ │ │ ├── apple-splash-1334-750.jpg │ │ │ ├── apple-splash-1488-2266.jpg │ │ │ ├── apple-splash-1536-2048.jpg │ │ │ ├── apple-splash-1620-2160.jpg │ │ │ ├── apple-splash-1640-2360.jpg │ │ │ ├── apple-splash-1668-2224.jpg │ │ │ ├── apple-splash-1668-2388.jpg │ │ │ ├── apple-splash-1792-828.jpg │ │ │ ├── apple-splash-2048-1536.jpg │ │ │ ├── apple-splash-2048-2732.jpg │ │ │ ├── apple-splash-2160-1620.jpg │ │ │ ├── apple-splash-2208-1242.jpg │ │ │ ├── apple-splash-2224-1668.jpg │ │ │ ├── apple-splash-2266-1488.jpg │ │ │ ├── apple-splash-2360-1640.jpg │ │ │ ├── apple-splash-2388-1668.jpg │ │ │ ├── apple-splash-2436-1125.jpg │ │ │ ├── apple-splash-2532-1170.jpg │ │ │ ├── apple-splash-2556-1179.jpg │ │ │ ├── apple-splash-2622-1206.jpg │ │ │ ├── apple-splash-2688-1242.jpg │ │ │ ├── apple-splash-2732-2048.jpg │ │ │ ├── apple-splash-2736-1260.jpg │ │ │ ├── apple-splash-2778-1284.jpg │ │ │ ├── apple-splash-2796-1290.jpg │ │ │ ├── apple-splash-2868-1320.jpg │ │ │ ├── apple-splash-640-1136.jpg │ │ │ ├── apple-splash-750-1334.jpg │ │ │ ├── apple-splash-828-1792.jpg │ │ │ ├── manifest-icon-192.maskable.png │ │ │ └── manifest-icon-512.maskable.png │ │ ├── images │ │ │ ├── categories │ │ │ │ ├── all.png │ │ │ │ ├── appliances.png │ │ │ │ ├── books.png │ │ │ │ ├── clothing.png │ │ │ │ ├── electronics.png │ │ │ │ ├── food.png │ │ │ │ ├── furniture.png │ │ │ │ ├── others.png │ │ │ │ ├── sports.png │ │ │ │ └── transport.png │ │ │ ├── google_form.png │ │ │ ├── hero_assets │ │ │ │ ├── 1.webp │ │ │ │ ├── 2.webp │ │ │ │ ├── 3.webp │ │ │ │ ├── 4.webp │ │ │ │ └── 5.webp │ │ │ ├── miniapp-resized.webp │ │ │ ├── miniapp.webp │ │ │ ├── nu-space-presentation.jpg │ │ │ ├── teams │ │ │ │ ├── adil.jpg │ │ │ │ ├── aisana.jpg │ │ │ │ ├── alan.jpg │ │ │ │ ├── bakhtiyar.jpg │ │ │ │ ├── ulan.jpg │ │ │ │ └── yelnur.jpg │ │ │ └── welcome-nu-space.jpg │ │ ├── manifest.json │ │ └── svg │ │ │ ├── Vector.svg │ │ │ ├── nuspace_logo.svg │ │ │ ├── profile-placeholder.svg │ │ │ └── telegram-connected.svg │ ├── components │ │ ├── animations │ │ │ ├── AnimatedCard.tsx │ │ │ ├── AnimatedFormField.tsx │ │ │ └── FloatingElements.tsx │ │ ├── atoms │ │ │ ├── accordion.tsx │ │ │ ├── alert.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── category-card.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── header.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── modal.tsx │ │ │ ├── motion-wrapper.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── select.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider-button.tsx │ │ │ ├── sonner.tsx │ │ │ ├── spinner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle.tsx │ │ │ └── tooltip.tsx │ │ ├── molecules │ │ │ ├── BackButton.tsx │ │ │ ├── DeleteConfirmation.tsx │ │ │ ├── MobileBottomNav.tsx │ │ │ ├── auth-required-alert.tsx │ │ │ ├── buttons │ │ │ │ ├── app-button.tsx │ │ │ │ ├── bind-telegram-button.tsx │ │ │ │ ├── channel-button.tsx │ │ │ │ ├── donate-button.tsx │ │ │ │ ├── donate-modal.tsx │ │ │ │ ├── login-button.tsx │ │ │ │ ├── message-button.tsx │ │ │ │ ├── report-button.tsx │ │ │ │ └── submit-button.tsx │ │ │ ├── combined-search.tsx │ │ │ ├── condition-dropdown.tsx │ │ │ ├── condition-group.tsx │ │ │ ├── filter-bar.tsx │ │ │ ├── general-section.tsx │ │ │ ├── hoc │ │ │ │ └── with-suspense.tsx │ │ │ ├── last-commit.tsx │ │ │ ├── login-modal.tsx │ │ │ ├── login-requirement-modal.tsx │ │ │ ├── nav-tabs.tsx │ │ │ ├── pagination.tsx │ │ │ ├── privacy-modal.tsx │ │ │ ├── search-input.tsx │ │ │ ├── slider-container.tsx │ │ │ ├── telegram-status.tsx │ │ │ ├── theme-toggle.tsx │ │ │ └── verification-badge.tsx │ │ ├── organisms │ │ │ ├── about │ │ │ │ ├── about-header.tsx │ │ │ │ ├── about-us-section.tsx │ │ │ │ ├── feature-card.tsx │ │ │ │ ├── mission-section.tsx │ │ │ │ ├── report-card.tsx │ │ │ │ ├── team-card.tsx │ │ │ │ └── team-member-card.tsx │ │ │ ├── admin │ │ │ │ ├── recent-card.tsx │ │ │ │ ├── stat-card.tsx │ │ │ │ └── user-card.tsx │ │ │ ├── animations │ │ │ │ ├── AnimatedCard.tsx │ │ │ │ ├── AnimatedFormField.tsx │ │ │ │ └── FloatingElements.tsx │ │ │ ├── app-grid.tsx │ │ │ ├── category-grid.tsx │ │ │ ├── category-slider.tsx │ │ │ ├── emergency-info-section.tsx │ │ │ ├── filter-container.tsx │ │ │ ├── glow-carousel-with-images.tsx │ │ │ └── media │ │ │ │ ├── MIGRATION_EXAMPLES.md │ │ │ │ ├── MediaPreview.tsx │ │ │ │ ├── README.md │ │ │ │ ├── UnifiedMediaUploadZone.tsx │ │ │ │ └── index.ts │ │ ├── templates │ │ │ └── about-template.tsx │ │ ├── ui │ │ │ └── footer.tsx │ │ └── virtual │ │ │ ├── InfiniteList.tsx │ │ │ ├── SearchableInfiniteList.tsx │ │ │ └── VirtualInfiniteList.tsx │ ├── context │ │ ├── BackNavigationContext.tsx │ │ ├── CommunityFormContext.tsx │ │ ├── EventFormContext.tsx │ │ ├── ListingContext.tsx │ │ ├── MediaEditContext.tsx │ │ ├── MediaUploadContext.tsx │ │ └── ThemeProviderContext.tsx │ ├── data │ │ ├── about │ │ │ └── team-members.tsx │ │ ├── features.ts │ │ ├── kp │ │ │ └── product.tsx │ │ ├── routes.tsx │ │ └── temporary │ │ │ └── index.ts │ ├── features │ │ ├── campuscurrent │ │ │ ├── communities │ │ │ │ ├── api │ │ │ │ │ ├── communitiesApi.ts │ │ │ │ │ └── hooks │ │ │ │ │ │ └── usePreSearchCommunities.ts │ │ │ │ ├── components │ │ │ │ │ ├── CommunityCard.tsx │ │ │ │ │ ├── CommunityModal.tsx │ │ │ │ │ ├── CommunitySelectionModal.tsx │ │ │ │ │ ├── UnifiedCommunityMediaUpload.tsx │ │ │ │ │ └── forms │ │ │ │ │ │ ├── CommunityActions.tsx │ │ │ │ │ │ ├── CommunityDescription.tsx │ │ │ │ │ │ └── CommunityDetailsForm.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── use-communities.ts │ │ │ │ │ ├── use-community.ts │ │ │ │ │ ├── use-edit-community.ts │ │ │ │ │ ├── use-search-communities.ts │ │ │ │ │ ├── use-user-communities.ts │ │ │ │ │ ├── useCreateCommunity.ts │ │ │ │ │ ├── useDeleteCommunity.ts │ │ │ │ │ ├── useInfiniteCommunities.ts │ │ │ │ │ ├── useUpdateCommunity.ts │ │ │ │ │ └── useVirtualCommunities.ts │ │ │ │ └── pages │ │ │ │ │ ├── list.tsx │ │ │ │ │ └── single.tsx │ │ │ ├── events │ │ │ │ ├── api │ │ │ │ │ └── eventsApi.ts │ │ │ │ ├── components │ │ │ │ │ ├── CountdownBadge.tsx │ │ │ │ │ ├── CountdownHeaderBar.tsx │ │ │ │ │ ├── CountdownOverlay.tsx │ │ │ │ │ ├── EventCard.tsx │ │ │ │ │ ├── EventModal.tsx │ │ │ │ │ ├── UnifiedEventMediaUpload.tsx │ │ │ │ │ └── forms │ │ │ │ │ │ ├── EventActions.tsx │ │ │ │ │ │ ├── EventDateTimeSelector.tsx │ │ │ │ │ │ ├── EventDescription.tsx │ │ │ │ │ │ ├── EventDetailsForm.tsx │ │ │ │ │ │ ├── EventElevatedFields.tsx │ │ │ │ │ │ └── EventScopeSelector.tsx │ │ │ │ ├── hooks │ │ │ │ │ ├── useCreateEvent.ts │ │ │ │ │ ├── useDeleteEvent.ts │ │ │ │ │ ├── useEvent.ts │ │ │ │ │ ├── useEvents.ts │ │ │ │ │ ├── useInfiniteEvents.ts │ │ │ │ │ ├── useUpdateEvent.ts │ │ │ │ │ └── useVirtualEvents.ts │ │ │ │ ├── pages │ │ │ │ │ ├── list.tsx │ │ │ │ │ └── single.tsx │ │ │ │ └── utils │ │ │ │ │ └── calendar.ts │ │ │ ├── pages │ │ │ │ ├── layout.tsx │ │ │ │ └── profile.tsx │ │ │ ├── subspace │ │ │ │ ├── api │ │ │ │ │ ├── hooks │ │ │ │ │ │ ├── useCreatePost.ts │ │ │ │ │ │ ├── useDeletePost.ts │ │ │ │ │ │ ├── useInfinitePosts.ts │ │ │ │ │ │ ├── usePost.ts │ │ │ │ │ │ ├── usePosts.ts │ │ │ │ │ │ ├── usePreSearchPosts.ts │ │ │ │ │ │ └── useUpdatePost.ts │ │ │ │ │ └── subspaceApi.ts │ │ │ │ ├── components │ │ │ │ │ ├── SubspaceEditModal.tsx │ │ │ │ │ ├── SubspacePostCard.tsx │ │ │ │ │ ├── SubspacePostModal.tsx │ │ │ │ │ ├── SubspacePosts.tsx │ │ │ │ │ └── SubspacePostsVirtual.tsx │ │ │ │ ├── hooks │ │ │ │ │ └── useVirtualPosts.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pages │ │ │ │ │ └── list.tsx │ │ │ │ └── types.ts │ │ │ └── types │ │ │ │ ├── event-sections.ts │ │ │ │ ├── nav-tabs.tsx │ │ │ │ └── types.ts │ │ ├── grade-statistics │ │ │ ├── api │ │ │ │ ├── gradeStatisticsApi.ts │ │ │ │ └── hooks │ │ │ │ │ └── usePreSearchGrades.ts │ │ │ ├── components │ │ │ │ ├── ConfirmationModal.tsx │ │ │ │ ├── GradeDistributionChart.tsx │ │ │ │ ├── GradeStatisticsCard.tsx │ │ │ │ ├── RegisteredCourseCard.tsx │ │ │ │ ├── RegisteredCourseItem.tsx │ │ │ │ └── TrendIndicator.tsx │ │ │ ├── pages │ │ │ │ └── GradeStatisticsPage.tsx │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── gradeUtils.ts │ │ │ │ └── templateUtils.ts │ │ ├── kupi-prodai │ │ │ ├── api │ │ │ │ ├── hooks │ │ │ │ │ ├── useCreateProduct.ts │ │ │ │ │ ├── useDeleteProduct.ts │ │ │ │ │ ├── usePreSearchProducts.ts │ │ │ │ │ ├── useProduct.ts │ │ │ │ │ ├── useProducts.ts │ │ │ │ │ ├── useToggleProduct.ts │ │ │ │ │ ├── useUpdateProduct.ts │ │ │ │ │ └── useUserProducts.ts │ │ │ │ └── kupiProdaiApi.ts │ │ │ ├── components │ │ │ │ ├── auth │ │ │ │ │ ├── AuthenticationGuard.tsx │ │ │ │ │ ├── LoginPromptCard.tsx │ │ │ │ │ └── TelegramPromptCard.tsx │ │ │ │ ├── common │ │ │ │ │ ├── ProductGrid.tsx │ │ │ │ │ └── ProductListingSection.tsx │ │ │ │ ├── forms │ │ │ │ │ ├── UnifiedProductForm.tsx │ │ │ │ │ ├── UnifiedProductMediaUpload.tsx │ │ │ │ │ ├── fields │ │ │ │ │ │ ├── ProductCategoryField.tsx │ │ │ │ │ │ ├── ProductConditionField.tsx │ │ │ │ │ │ ├── ProductDescriptionField.tsx │ │ │ │ │ │ ├── ProductNameField.tsx │ │ │ │ │ │ └── ProductPriceField.tsx │ │ │ │ │ └── sections │ │ │ │ │ │ ├── BasicInfoSection.tsx │ │ │ │ │ │ └── FormActionsSection.tsx │ │ │ │ ├── main-page │ │ │ │ │ ├── BuySection.tsx │ │ │ │ │ ├── SellSection.tsx │ │ │ │ │ └── my-listings │ │ │ │ │ │ ├── EditListingForm.tsx │ │ │ │ │ │ ├── EmptyState.tsx │ │ │ │ │ │ ├── MyListingsSection.tsx │ │ │ │ │ │ └── UnifiedEditListingModal.tsx │ │ │ │ ├── product-card-actions.tsx │ │ │ │ ├── product-card.tsx │ │ │ │ ├── product-create-form.tsx │ │ │ │ ├── product-detail-page │ │ │ │ │ ├── ContactSellerModal.tsx │ │ │ │ │ ├── ImageViewerModal.tsx │ │ │ │ │ ├── ProductDetails.tsx │ │ │ │ │ ├── ProductImageCarousel.tsx │ │ │ │ │ ├── ProductPageActions.tsx │ │ │ │ │ └── ReportListingModal.tsx │ │ │ │ ├── state │ │ │ │ │ ├── product-empy-state.tsx │ │ │ │ │ ├── product-error-state.tsx │ │ │ │ │ └── product-loading-state.tsx │ │ │ │ └── tables │ │ │ │ │ ├── products-table.tsx │ │ │ │ │ └── users-table.tsx │ │ │ ├── hooks │ │ │ │ ├── useEditModal.ts │ │ │ │ └── useProductForm.ts │ │ │ ├── pages │ │ │ │ ├── home.tsx │ │ │ │ └── product │ │ │ │ │ └── [id].tsx │ │ │ └── types.ts │ │ ├── media │ │ │ ├── UNIFIED_MIGRATION_GUIDE.md │ │ │ ├── api │ │ │ │ └── mediaApi.ts │ │ │ ├── config │ │ │ │ └── mediaConfigs.ts │ │ │ ├── context │ │ │ │ └── UnifiedMediaContext.tsx │ │ │ ├── hooks │ │ │ │ ├── useInitializeMedia.ts │ │ │ │ ├── useMediaEdit.ts │ │ │ │ ├── useMediaSelection.ts │ │ │ │ ├── useMediaUpload.ts │ │ │ │ └── useUnifiedMedia.ts │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ ├── media.ts │ │ │ │ └── types.ts │ │ │ └── utils │ │ │ │ ├── compress-media.ts │ │ │ │ ├── get-signed-urls.ts │ │ │ │ └── upload-media.ts │ │ └── sgotinish │ │ │ ├── api │ │ │ └── sgotinishApi.ts │ │ │ ├── components │ │ │ ├── Conversation.tsx │ │ │ ├── CreateAppealButton.tsx │ │ │ ├── CreateTicketModal.tsx │ │ │ ├── DelegateModal.tsx │ │ │ ├── SGDashboard.tsx │ │ │ ├── StudentDashboard.tsx │ │ │ ├── TelegramConnectCard.tsx │ │ │ ├── TicketCard.tsx │ │ │ └── TicketDetail.tsx │ │ │ ├── pages │ │ │ └── SgotinishPage.tsx │ │ │ ├── types.ts │ │ │ └── utils │ │ │ ├── date.ts │ │ │ └── roleMapping.ts │ ├── hooks │ │ ├── use-search-logic.ts │ │ ├── use-toast.ts │ │ ├── use-user.ts │ │ ├── useDebounce.ts │ │ ├── useFormAnimations.ts │ │ ├── useGlobalSecondTicker.ts │ │ ├── useInfiniteScroll.ts │ │ ├── usePageParam.ts │ │ ├── useTelegramBottomButtons.ts │ │ ├── useTelegramMiniApp.ts │ │ └── useVirtualInfiniteScroll.ts │ ├── pages │ │ ├── about.tsx │ │ ├── admin │ │ │ ├── admin-layout.tsx │ │ │ ├── admin-page.tsx │ │ │ ├── product-detail-page.tsx │ │ │ ├── products-page.tsx │ │ │ ├── user-page.tsx │ │ │ └── users-page.tsx │ │ ├── apps-layout.tsx │ │ ├── apps │ │ │ ├── dorm-eats.tsx │ │ │ └── emergency.tsx │ │ ├── home.tsx │ │ └── profile.tsx │ ├── types │ │ ├── images.d.ts │ │ ├── index.d.ts │ │ └── search.ts │ ├── utils │ │ ├── animationVariants.ts │ │ ├── api.ts │ │ ├── date-formatter.ts │ │ ├── image-utils.tsx │ │ ├── polling.ts │ │ ├── products-utils.ts │ │ ├── query-client.ts │ │ ├── search-params.ts │ │ └── utils.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── infra ├── .env.example ├── README.md ├── alloy │ └── config.alloy ├── build.docker-compose.yaml ├── docker-compose.yml ├── grafana │ ├── dashboards │ │ ├── Node Exporter Full-1752429426828.json │ │ ├── cAdvisor Docker Insights-1752429437635.json │ │ └── dashboard.json │ └── provisioning │ │ ├── alerting │ │ ├── alerting.yaml │ │ ├── alerts.yaml │ │ └── contact-points.yaml.tpl │ │ ├── dashboards │ │ └── default.yaml │ │ └── datasources │ │ └── datasources.yml ├── loki │ ├── loki.prod.yaml │ └── loki.yaml ├── nginx │ ├── nginx.conf │ ├── nginx.dev.conf │ └── vpn-index.html ├── pgadmin │ ├── pgpass │ └── servers.json ├── prod.docker-compose.yml ├── prometheus │ ├── ALERTMANAGER_SETUP.md │ ├── alertmanager.yml.tpl │ ├── grafana_alert_rules.yml │ └── prometheus.yml ├── rabbitmq │ └── rabbitmq.conf ├── redis │ └── redis.conf ├── scripts │ ├── start-alertmanager.sh │ └── start-grafana.sh └── wireguard │ └── README.md └── terraform ├── .terraform.lock.hcl ├── README.md ├── apis.tf ├── backend.tf ├── backend.tfbackend ├── compute.tf ├── envs ├── production.tfvars └── staging.tfvars ├── iam.tf ├── outputs.tf ├── providers.tf ├── pubsub.tf ├── storage.tf ├── tfscheme.png └── variables.tf /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Bug Description** 11 | Clear description of the issue. 12 | 13 | **Steps to Reproduce** 14 | 1. 15 | 16 | **Expected Behavior** 17 | What should happen instead. 18 | 19 | **Screenshots (optional)** 20 | Add if helpful. 21 | 22 | **Environment (optional)** 23 | - OS: 24 | - Browser/App: 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python cache files (ignore __pycache__ in any directory) 2 | **/__pycache__/ 3 | **/*.pyc 4 | **/*.pyo 5 | **/*.pyd 6 | 7 | # Virtual environment 8 | venv/ 9 | 10 | # Terraform credentials and state files 11 | **/terraform.json 12 | terraform/creds/ 13 | *.tfstate 14 | *.tfstate.* 15 | .terraform/ 16 | 17 | # PyCharm IDE files 18 | .idea/ 19 | 20 | # Node.js dependencies 21 | node_modules/ 22 | 23 | # Build output (Vite/React) 24 | dist/ 25 | ssl/ 26 | # Static folder pycache 27 | static/**/__pycache__/ 28 | 29 | # Configs folder pycache 30 | configs/**/__pycache__/ 31 | 32 | # Environmental files 33 | .env 34 | 35 | # Cloudflared local dev credentials 36 | infra/cloudflared/ 37 | 38 | .ansible/ 39 | 40 | # Log files 41 | *.log 42 | 43 | # OS-specific files 44 | .DS_Store 45 | **/.DS_Store 46 | Thumbs.db 47 | 48 | 49 | **/.vscode/ 50 | 51 | # Loki bucket service account 52 | **/nuspace_bucket.json 53 | 54 | **/.ruff_cache/ 55 | **/.pytest_cache/ 56 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.11.5 # актуальную версию можно посмотреть на GitHub 4 | hooks: 5 | - id: ruff 6 | args: [--fix] 7 | 8 | - repo: https://github.com/psf/black 9 | rev: 24.3.0 10 | hooks: 11 | - id: black 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ulan Sharipov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /ansible/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | all: 3 | children: 4 | webservers: 5 | hosts: 6 | # This will be populated by the GitHub Actions workflow 7 | # with the actual host from secrets.ANSIBLE_HOST 8 | -------------------------------------------------------------------------------- /ansible/roles/backend/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | -------------------------------------------------------------------------------- /ansible/roles/container_analysis/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | -------------------------------------------------------------------------------- /ansible/roles/frontend/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | -------------------------------------------------------------------------------- /ansible/roles/infra_services/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | -------------------------------------------------------------------------------- /ansible/roles/post_tasks/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Post-deployment tasks 3 | # DOCKER_IMAGE_TAG environment variable is no longer needed since we use 'latest' tag directly 4 | 5 | -------------------------------------------------------------------------------- /ansible/roles/repo_sync/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Pull latest changes from git repository 3 | ansible.builtin.git: 4 | repo: "https://github.com/ulanpy/nuspace.git" 5 | dest: "/home/{{ ansible_user }}/nuspace" 6 | version: "{{ github_ref_name | default('main') }}" 7 | force: yes 8 | 9 | 10 | -------------------------------------------------------------------------------- /ansible/roles/secrets/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use official Python 3.12 slim image 2 | FROM python:3.12-alpine3.22 3 | 4 | # Set build arguments 5 | ARG IS_DEBUG=true 6 | 7 | # Set timezone and Python settings 8 | ENV TZ=UTC \ 9 | PYTHONUNBUFFERED=1 \ 10 | PYTHONDONTWRITEBYTECODE=1 \ 11 | PYTHONPATH=/nuros \ 12 | POETRY_NO_INTERACTION=1 \ 13 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 14 | POETRY_VIRTUALENVS_CREATE=0 \ 15 | POETRY_CACHE_DIR=/tmp/poetry_cache 16 | 17 | WORKDIR /nuros 18 | ENV PATH="/root/.local/bin:${PATH}" 19 | 20 | RUN apk -u add \ 21 | build-base \ 22 | python3-dev \ 23 | libpq-dev \ 24 | gcc \ 25 | g++ \ 26 | musl-dev \ 27 | linux-headers \ 28 | && pip install --no-cache-dir poetry 29 | 30 | 31 | COPY backend/pyproject.toml backend/poetry.lock ./ 32 | COPY backend/start.sh / 33 | RUN poetry install --no-root --no-cache 34 | 35 | COPY .. . 36 | 37 | COPY backend/start.sh / 38 | RUN sed -i 's/\r$//' /start.sh 39 | RUN chmod +x /start.sh 40 | RUN apk add --no-cache bash 41 | # Run the script to decide the server behavior based on IS_DEBUG 42 | CMD ["/start.sh"] 43 | -------------------------------------------------------------------------------- /backend/Dockerfile_celery: -------------------------------------------------------------------------------- 1 | # Use official Python 3.12 slim image 2 | FROM python:3.12-alpine3.22 3 | 4 | # Set timezone and Python settings 5 | ENV TZ=UTC \ 6 | PYTHONUNBUFFERED=1 \ 7 | PYTHONPATH=/nuros \ 8 | POETRY_NO_INTERACTION=1 \ 9 | POETRY_VIRTUALENVS_IN_PROJECT=1 \ 10 | POETRY_VIRTUALENVS_CREATE=0 \ 11 | POETRY_CACHE_DIR=/tmp/poetry_cache 12 | 13 | # Configure system dependencies and paths 14 | WORKDIR /nuros 15 | ENV PATH="/root/.local/bin:${PATH}" 16 | 17 | # Install system dependencies and build tools 18 | RUN apk -u add \ 19 | build-base \ 20 | python3-dev \ 21 | libpq-dev \ 22 | build-base \ 23 | && pip install --no-cache-dir poetry 24 | 25 | # Copy dependency files first for better layer caching 26 | COPY backend/pyproject.toml backend/poetry.lock ./ 27 | 28 | # Copy application code 29 | COPY .. . 30 | 31 | # Install project dependencies 32 | RUN poetry install --no-root --no-cache 33 | -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/__init__.py -------------------------------------------------------------------------------- /backend/app_state/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/app_state/__init__.py -------------------------------------------------------------------------------- /backend/app_state/db.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from backend.core.configs.config import config 4 | from backend.core.database.manager import AsyncDatabaseManager 5 | 6 | 7 | async def setup_db(app: FastAPI): 8 | app.state.db_manager = AsyncDatabaseManager() 9 | # Avoid implicit schema creation in production – rely on Alembic migrations instead 10 | 11 | # === When modifying tables, comment this out! === 12 | if config.IS_DEBUG: 13 | await app.state.db_manager.create_all_tables() 14 | 15 | 16 | async def cleanup_db(app: FastAPI): 17 | db_manager = getattr(app.state, "db_manager", None) 18 | if db_manager: 19 | await db_manager.async_engine.dispose() 20 | -------------------------------------------------------------------------------- /backend/app_state/rbq.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from faststream.rabbit import RabbitBroker 3 | 4 | from backend.modules.notification import tasks 5 | 6 | 7 | async def setup_rbq(app: FastAPI): 8 | broker: RabbitBroker = tasks.broker 9 | await broker.connect() 10 | await broker.start() 11 | 12 | app.state.broker = broker 13 | 14 | 15 | async def cleanup_rbq(app: FastAPI): 16 | broker: RabbitBroker = tasks.broker 17 | await broker.stop() 18 | app.state.broker = None 19 | -------------------------------------------------------------------------------- /backend/app_state/redis.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from redis.asyncio import ConnectionPool, Redis 3 | 4 | from backend.core.configs.config import config 5 | 6 | 7 | async def setup_redis(app: FastAPI): 8 | redis_pool = ConnectionPool.from_url( 9 | config.REDIS_URL, 10 | max_connections=50, 11 | socket_connect_timeout=5, 12 | socket_timeout=10, 13 | health_check_interval=30, 14 | retry_on_timeout=True, 15 | decode_responses=True, 16 | ) 17 | app.state.redis = Redis(connection_pool=redis_pool) 18 | 19 | 20 | async def cleanup_redis(app: FastAPI): 21 | if redis := getattr(app.state, "redis", None): 22 | await redis.aclose() 23 | -------------------------------------------------------------------------------- /backend/celery_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/celery_app/__init__.py -------------------------------------------------------------------------------- /backend/celery_app/celery_config.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | 3 | from backend.core.configs.config import config 4 | 5 | celery_app = Celery( 6 | main="worker", 7 | broker=config.CELERY_BROKER_URL, 8 | include=["backend.celery_app.tasks"], 9 | broker_connection_retry_on_startup=True, # Important for Docker compatibility 10 | ) 11 | 12 | celery_app.conf.update( 13 | task_serializer="json", 14 | result_serializer="json", 15 | accept_content=["json"], 16 | timezone="UTC", 17 | enable_utc=True, 18 | worker_send_task_events=True, 19 | task_ignore_result=True, # We don't need results for kick tasks 20 | task_acks_late=True, # Better for reliability. Means worker ACK broker only when done executing 21 | task_reject_on_worker_lost=True, 22 | task_track_started=True, 23 | broker_connection_retry_on_startup=True, 24 | worker_prefetch_multiplier=1, # Better for fairness gives prefetch_count=1 to RMQ 25 | task_soft_time_limit=30, # 30 seconds timeout 26 | task_default_queue="default", 27 | task_routes={"backend.celery_app.tasks.schedule_kick": {"queue": "kick_queue"}}, 28 | ) 29 | -------------------------------------------------------------------------------- /backend/celery_app/tasks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import Bot 4 | 5 | from backend.core.configs.config import config 6 | 7 | from .celery_config import celery_app 8 | 9 | 10 | @celery_app.task 11 | def schedule_kick(chat_id: int, user_id: int, message_id: int): 12 | loop = asyncio.new_event_loop() 13 | asyncio.set_event_loop(loop) 14 | 15 | async def kick_async(chat_id: int, user_id: int, message_id: int): 16 | bot = Bot(token=config.TELEGRAM_BOT_TOKEN) 17 | await bot.ban_chat_member(chat_id, user_id) 18 | await bot.unban_chat_member(chat_id, user_id) 19 | await bot.delete_message(chat_id=chat_id, message_id=message_id) 20 | 21 | await bot.session.close() 22 | 23 | try: 24 | result = loop.run_until_complete(kick_async(chat_id, user_id, message_id)) 25 | return result 26 | finally: 27 | loop.close() 28 | -------------------------------------------------------------------------------- /backend/common/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from backend.core.configs.config import Config 4 | from backend.core.database.models.common_enums import EntityType 5 | from backend.core.database.models.media import MediaFormat 6 | from google.auth.credentials import Credentials 7 | from google.cloud import storage 8 | from httpx import AsyncClient 9 | from redis.asyncio import Redis 10 | from faststream.rabbit import RabbitBroker 11 | from pydantic import BaseModel 12 | 13 | 14 | class MediaResponse(BaseModel): 15 | id: int 16 | url: str 17 | mime_type: str 18 | entity_type: EntityType 19 | entity_id: int 20 | media_format: MediaFormat 21 | media_order: int 22 | 23 | 24 | class ShortUserResponse(BaseModel): 25 | sub: str 26 | name: str 27 | surname: str 28 | picture: str 29 | 30 | class Config: 31 | from_attributes = True 32 | 33 | 34 | class ResourcePermissions(BaseModel): 35 | can_edit: bool = False 36 | can_delete: bool = False 37 | editable_fields: List[str] = [] 38 | 39 | 40 | class Infra(BaseModel): 41 | """Infrastructure dependencies for event operations.""" 42 | 43 | meilisearch_client: AsyncClient 44 | storage_client: storage.Client 45 | config: Config 46 | signing_credentials: Credentials | None = None 47 | redis: Redis 48 | broker: RabbitBroker 49 | class Config: 50 | arbitrary_types_allowed = True 51 | -------------------------------------------------------------------------------- /backend/common/utils/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ResourceAction(str, Enum): 5 | CREATE = "create" 6 | READ = "read" 7 | UPDATE = "update" 8 | DELETE = "delete" 9 | -------------------------------------------------------------------------------- /backend/core/configs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/core/configs/__init__.py -------------------------------------------------------------------------------- /backend/core/configs/coverpage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/core/configs/coverpage.jpg -------------------------------------------------------------------------------- /backend/core/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/core/database/__init__.py -------------------------------------------------------------------------------- /backend/core/database/manager.py: -------------------------------------------------------------------------------- 1 | from typing import AsyncGenerator 2 | 3 | from backend.core.configs.config import config 4 | from backend.core.database.models import Base 5 | # from backend.core.database.models import Base 6 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 7 | 8 | 9 | class AsyncDatabaseManager: 10 | def __init__(self): 11 | self.async_engine = create_async_engine( 12 | config.DATABASE_URL, 13 | query_cache_size=1200, 14 | pool_size=20, 15 | max_overflow=200, 16 | future=True, 17 | echo=False, 18 | ) 19 | self.async_session_maker = async_sessionmaker( 20 | bind=self.async_engine, 21 | expire_on_commit=False, 22 | ) 23 | 24 | # === Deprecated. Will be removed starting from October 2025 === 25 | async def create_all_tables(self) -> None: 26 | async with self.async_engine.begin() as conn: 27 | await conn.run_sync(Base.metadata.create_all) 28 | await self.async_engine.dispose() 29 | 30 | # this function returns async session used in fastapi dependency injections 31 | async def get_async_session(self) -> AsyncGenerator[AsyncSession, None]: 32 | async with self.async_session_maker() as session: 33 | try: 34 | yield session 35 | finally: 36 | await session.close() 37 | -------------------------------------------------------------------------------- /backend/core/database/models/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base 2 | 3 | Base: DeclarativeMeta = declarative_base() 4 | -------------------------------------------------------------------------------- /backend/core/database/models/media.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum as PyEnum 3 | 4 | from sqlalchemy import BigInteger, Column, DateTime, Integer 5 | from sqlalchemy import Enum as SQLEnum 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | from .base import Base 9 | from .common_enums import EntityType 10 | 11 | 12 | class MediaFormat(PyEnum): 13 | banner = "banner" 14 | carousel = "carousel" 15 | profile = "profile" 16 | 17 | 18 | # Mapped[dtype] defaults parameters: nullable=False, unique=True, primary_key=False 19 | class Media(Base): 20 | __tablename__ = "media" 21 | id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) 22 | name: Mapped[str] = mapped_column(nullable=False, index=True, unique=True) 23 | mime_type: Mapped[str] = mapped_column(nullable=False, unique=False) 24 | entity_type: Mapped[EntityType] = mapped_column( 25 | SQLEnum(EntityType, name="entity_type"), nullable=False, index=True 26 | ) 27 | entity_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True, unique=False) 28 | media_format: Mapped[MediaFormat] = mapped_column( 29 | SQLEnum(MediaFormat, name="media_format"), nullable=False, index=True 30 | ) 31 | media_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) 32 | created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 33 | updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) 34 | -------------------------------------------------------------------------------- /backend/core/database/models/notification.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from backend.core.database.models.base import Base 4 | from backend.core.database.models.common_enums import EntityType, NotificationType 5 | from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | 9 | class Notification(Base): 10 | __tablename__ = "notifications" 11 | 12 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 13 | title: Mapped[str] = mapped_column(String, nullable=False) 14 | message: Mapped[str] = mapped_column(String, nullable=False) 15 | notification_source: Mapped[EntityType] = mapped_column(String, nullable=False) 16 | receiver_sub: Mapped[str] = mapped_column(ForeignKey("users.sub"), nullable=False) 17 | type: Mapped[NotificationType] = mapped_column(String, nullable=False) 18 | tg_id: Mapped[int] = mapped_column(BigInteger, nullable=False) 19 | url: Mapped[str] = mapped_column(String, nullable=True) 20 | created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 21 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from starlette.middleware.sessions import SessionMiddleware 4 | from fastapi.responses import ORJSONResponse 5 | 6 | from backend.core.configs.config import config 7 | from backend.lifespan import lifespan 8 | 9 | # Import both the instrumentor and the metrics_app 10 | from backend.middlewares.prometheus_metrics import instrument_app, metrics_app 11 | 12 | app = FastAPI( 13 | debug=config.IS_DEBUG, 14 | lifespan=lifespan, 15 | default_response_class=ORJSONResponse, # for performance 16 | root_path="/api", 17 | docs_url="/docs" if config.IS_DEBUG else None, 18 | redoc_url="/redoc" if config.IS_DEBUG else None, 19 | openapi_url="/openapi.json" if config.IS_DEBUG else None, 20 | title="NU Space API", 21 | description=" Nuspace.kz is a SuperApp for NU students that streamlines communication and " 22 | "replaces disorganized Telegram chats with a more reliable solution. " 23 | "[Project Github](https://github.com/ulanpy/nuspace). ", 24 | version="1.0.4", 25 | ) 26 | 27 | app.mount("/metrics", metrics_app) 28 | 29 | app.add_middleware( 30 | CORSMiddleware, 31 | allow_origins=["*"] if config.IS_DEBUG else config.ORIGINS, 32 | allow_credentials=True, 33 | allow_methods=["*"], 34 | allow_headers=["*"], 35 | ) 36 | app.add_middleware(SessionMiddleware, secret_key=config.SESSION_MIDDLEWARE_KEY) 37 | 38 | instrument_app(app) 39 | -------------------------------------------------------------------------------- /backend/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | ${upgrades if upgrades else "pass"} 24 | 25 | 26 | def downgrade() -> None: 27 | """Downgrade schema.""" 28 | ${downgrades if downgrades else "pass"} 29 | -------------------------------------------------------------------------------- /backend/migrations/versions/183fea04d6e8_added_verification_badge_to_communities.py: -------------------------------------------------------------------------------- 1 | """added verification badge to communities 2 | 3 | Revision ID: 183fea04d6e8 4 | Revises: 2659177a3d3a 5 | Create Date: 2025-08-12 14:40:32.496920 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | from sqlalchemy.dialects import postgresql 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '183fea04d6e8' 16 | down_revision: Union[str, Sequence[str], None] = '2659177a3d3a' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | # Add verified with a temporary server default to avoid NOT NULL violations on existing rows 24 | op.add_column( 25 | 'communities', 26 | sa.Column('verified', sa.Boolean(), server_default=sa.text('false'), nullable=False), 27 | ) 28 | # Drop the server default to match model (no default at the DB level) 29 | op.alter_column('communities', 'verified', server_default=None) 30 | 31 | 32 | def downgrade() -> None: 33 | """Downgrade schema.""" 34 | op.drop_column('communities', 'verified') 35 | -------------------------------------------------------------------------------- /backend/migrations/versions/18da0a04711e_set_default_false_on_communities_.py: -------------------------------------------------------------------------------- 1 | """Set default false on communities.verified 2 | 3 | Revision ID: 18da0a04711e 4 | Revises: 183fea04d6e8 5 | Create Date: 2025-08-14 12:06:44.489333 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '18da0a04711e' 16 | down_revision: Union[str, Sequence[str], None] = '183fea04d6e8' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | op.execute("UPDATE communities SET verified = false WHERE verified IS NULL") 24 | op.alter_column( 25 | 'communities', 26 | 'verified', 27 | server_default=sa.text('false'), 28 | existing_type=sa.Boolean(), 29 | existing_nullable=False, 30 | ) 31 | 32 | 33 | def downgrade() -> None: 34 | """Downgrade schema.""" 35 | op.alter_column( 36 | 'communities', 37 | 'verified', 38 | server_default=None, 39 | existing_type=sa.Boolean(), 40 | existing_nullable=False, 41 | ) 42 | -------------------------------------------------------------------------------- /backend/migrations/versions/2659177a3d3a_add_registration_link_to_events_table.py: -------------------------------------------------------------------------------- 1 | """Add registration_link to events table 2 | 3 | Revision ID: 2659177a3d3a 4 | Revises: 1ff9b347b5f3 5 | Create Date: 2025-08-12 05:48:54.903427 6 | 7 | """ 8 | 9 | from typing import Sequence, Union 10 | 11 | import sqlalchemy as sa 12 | from alembic import op 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = "2659177a3d3a" 16 | down_revision: Union[str, Sequence[str], None] = "1ff9b347b5f3" 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | # Add registration_link column to events table 24 | op.add_column("events", sa.Column("registration_link", sa.String(), nullable=True)) 25 | 26 | 27 | def downgrade() -> None: 28 | """Downgrade schema.""" 29 | # Remove registration_link column from events table 30 | op.drop_column("events", "registration_link") 31 | -------------------------------------------------------------------------------- /backend/migrations/versions/bcb1ca0ddd00_add_recruitment_to_event_type_enum.py: -------------------------------------------------------------------------------- 1 | """add recruitment to event_type enum 2 | 3 | Revision ID: bcb1ca0ddd00 4 | Revises: 18da0a04711e 5 | Create Date: 2025-08-15 12:06:04.604280 6 | 7 | """ 8 | 9 | from typing import Sequence, Union 10 | 11 | from alembic import op 12 | 13 | # revision identifiers, used by Alembic. 14 | revision: str = "bcb1ca0ddd00" 15 | down_revision: Union[str, Sequence[str], None] = "18da0a04711e" 16 | branch_labels: Union[str, Sequence[str], None] = None 17 | depends_on: Union[str, Sequence[str], None] = None 18 | 19 | 20 | def upgrade() -> None: 21 | """Upgrade schema.""" 22 | # Add recruitment to event_type enum 23 | op.execute("ALTER TYPE event_type ADD VALUE 'recruitment'") 24 | 25 | 26 | def downgrade() -> None: 27 | """Downgrade schema.""" 28 | # Note: PostgreSQL doesn't support removing enum values directly 29 | # This would require recreating the enum type, which is complex 30 | # For now, we'll leave the recruitment value in place during downgrade 31 | pass 32 | -------------------------------------------------------------------------------- /backend/migrations/versions/df3b09aac8a7_adding_cps_school.py: -------------------------------------------------------------------------------- 1 | """adding CPS school 2 | 3 | Revision ID: df3b09aac8a7 4 | Revises: ff17af4961b9 5 | Create Date: 2025-10-12 16:27:18.948648 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'df3b09aac8a7' 16 | down_revision: Union[str, Sequence[str], None] = 'ff17af4961b9' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.execute("ALTER TYPE school_type ADD VALUE 'CPS'") 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade() -> None: 29 | """Downgrade schema.""" 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | pass 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /backend/modules/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/auth/__init__.py -------------------------------------------------------------------------------- /backend/modules/auth/cruds.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import or_, select 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from backend.core.database.models import User 5 | from backend.modules.auth.schemas import UserSchema 6 | 7 | 8 | async def upsert_user(session: AsyncSession, user_schema: UserSchema) -> User: 9 | """ 10 | Upsert a user into the database 11 | """ 12 | # Query if the user already exists using the 'sub' field 13 | result = await session.execute( 14 | select(User).filter(or_(User.sub == user_schema.sub, User.email == user_schema.email)) 15 | ) 16 | user_db = result.scalars().first() 17 | 18 | if user_db: 19 | # User exists, update the user's information using unpacking 20 | for key, value in user_schema.model_dump().items(): 21 | if key not in ["role", "scope"]: # Exclude role and scope from updates 22 | setattr(user_db, key, value) 23 | else: 24 | # User does not exist, create a new user 25 | user_db = User(**user_schema.model_dump()) 26 | session.add(user_db) 27 | 28 | # Commit the session and refresh the user instance to get all the latest data 29 | await session.commit() 30 | await session.refresh(user_db) 31 | 32 | return user_db 33 | -------------------------------------------------------------------------------- /backend/modules/auth/dependencies.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/auth/dependencies.py -------------------------------------------------------------------------------- /backend/modules/auth/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from pydantic import BaseModel, EmailStr 4 | 5 | from backend.core.database.models.user import UserRole, UserScope 6 | 7 | 8 | class UserSchema(BaseModel): 9 | email: EmailStr 10 | role: UserRole 11 | scope: UserScope 12 | name: str 13 | surname: str 14 | picture: str 15 | sub: str 16 | 17 | 18 | class Sub(BaseModel): 19 | sub: str 20 | 21 | 22 | class CurrentUserResponse(BaseModel): 23 | user: Dict[str, Any] # This will store user token data 24 | tg_id: int | None = None # Indicates if user exists in the database 25 | -------------------------------------------------------------------------------- /backend/modules/bot/README.md: -------------------------------------------------------------------------------- 1 | ### Telegram Bot Localization binary compilation 2 | 3 | ```bash 4 | msgfmt backend/routes/bot/locales/ru/LC_MESSAGES/messages.po -o backend/routes/bot/locales/ru/LC_MESSAGES/messages.mo 5 | msgfmt backend/routes/bot/locales/en/LC_MESSAGES/messages.po -o backend/routes/bot/locales/en/LC_MESSAGES/messages.mo 6 | msgfmt backend/routes/bot/locales/kz/LC_MESSAGES/messages.po -o backend/routes/bot/locales/kz/LC_MESSAGES/messages.mo 7 | ``` -------------------------------------------------------------------------------- /backend/modules/bot/filters/__init__.py: -------------------------------------------------------------------------------- 1 | from .deeplink import EncodedDeepLinkFilter 2 | -------------------------------------------------------------------------------- /backend/modules/bot/filters/deeplink.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from aiogram.filters import BaseFilter, CommandObject 4 | from aiogram.types import Message 5 | from aiogram.utils.payload import decode_payload 6 | 7 | 8 | class EncodedDeepLinkFilter(BaseFilter): 9 | def __init__(self, prefix: Optional[str] = None): 10 | self.prefix = prefix 11 | 12 | async def __call__(self, message: Message, command: CommandObject) -> bool: 13 | args = command.args 14 | payload: str = decode_payload(args) 15 | 16 | if not payload: 17 | return False 18 | 19 | if self.prefix: 20 | if payload.startswith(self.prefix): 21 | return True 22 | return False 23 | return False 24 | -------------------------------------------------------------------------------- /backend/modules/bot/hints_command.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot 2 | from aiogram.types import BotCommand, BotCommandScopeAllPrivateChats 3 | 4 | 5 | async def set_commands(bot: Bot): 6 | start = BotCommand(command="start", description="🟢 Start") 7 | language = BotCommand(command="language", description="🟢 Change language") 8 | await bot.set_my_commands(commands=[start, language], scope=BotCommandScopeAllPrivateChats()) 9 | -------------------------------------------------------------------------------- /backend/modules/bot/keyboards/callback_factory.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from backend.modules.bot.utils.enums import NotificationEnum 3 | 4 | 5 | class ConfirmTelegramUser(CallbackData, prefix="confirm"): 6 | sub: str 7 | number: int 8 | confirmation_number: int 9 | 10 | 11 | class Languages(CallbackData, prefix="language"): 12 | language: str 13 | 14 | 15 | class NotificationAction(CallbackData, prefix="notif"): 16 | action: NotificationEnum 17 | -------------------------------------------------------------------------------- /backend/modules/bot/locales/en/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/bot/locales/en/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /backend/modules/bot/locales/kz/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/bot/locales/kz/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /backend/modules/bot/locales/ru/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/bot/locales/ru/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from google.cloud import storage 3 | from redis.asyncio import Redis 4 | 5 | from backend.core.database.manager import AsyncDatabaseManager 6 | 7 | from .bucket_client import BucketClientMiddleware 8 | from .db_session import DatabaseMiddleware 9 | from .i18n import I18N 10 | from .public_url import UrlMiddleware 11 | from .redis import RedisMiddleware 12 | 13 | 14 | def setup_middlewares( 15 | dp: Dispatcher, 16 | url: str, 17 | redis: Redis, 18 | db_manager: AsyncDatabaseManager, 19 | storage_client: storage.Client, 20 | ): 21 | middlewares = [ 22 | DatabaseMiddleware(db_manager), 23 | RedisMiddleware(redis), 24 | UrlMiddleware("https://t.me/NUspaceBot/app"), 25 | I18N(), 26 | BucketClientMiddleware(storage_client), 27 | ] 28 | for middleware in middlewares: 29 | dp.update.middleware(middleware) 30 | dp.message.middleware(middleware) 31 | dp.callback_query.middleware(middleware) 32 | dp.chat_member.middleware(middleware) 33 | -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/bucket_client.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import TelegramObject 5 | from google.cloud import storage 6 | 7 | 8 | class BucketClientMiddleware(BaseMiddleware): 9 | def __init__(self, storage_client: storage.Client) -> None: 10 | self.storage_client = storage_client 11 | 12 | async def __call__( 13 | self, 14 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], 15 | event: TelegramObject, 16 | data: dict[str, Any], 17 | ) -> Any: 18 | data["storage_client"] = self.storage_client 19 | return await handler(event, data) 20 | -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/db_session.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from typing import Any, Awaitable, Callable 3 | 4 | from aiogram import BaseMiddleware 5 | from aiogram.types import TelegramObject 6 | 7 | from backend.core.database.manager import AsyncDatabaseManager 8 | 9 | 10 | class DatabaseMiddleware(BaseMiddleware): 11 | def __init__(self, db_manager: AsyncDatabaseManager) -> None: 12 | self.db_manager = db_manager 13 | 14 | async def __call__( 15 | self, 16 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], 17 | event: TelegramObject, 18 | data: dict[str, Any], 19 | ) -> Any: 20 | 21 | async with contextlib.asynccontextmanager(self.db_manager.get_async_session)() as session: 22 | data["db_session"] = session 23 | return await handler(event, data) 24 | -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/i18n.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import os 3 | from pathlib import Path 4 | from typing import Any, Awaitable, Callable 5 | 6 | from aiogram import BaseMiddleware 7 | from aiogram.types import CallbackQuery, Message, TelegramObject 8 | from redis.asyncio import Redis 9 | 10 | 11 | def get_translator(lang: str): 12 | LOCALES_DIR = os.path.join(Path(__file__).parent.parent, "locales") 13 | translator = gettext.translation("messages", localedir=LOCALES_DIR, languages=[lang]) 14 | return translator.gettext 15 | 16 | 17 | class I18N(BaseMiddleware): 18 | 19 | async def __call__( 20 | self, 21 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], 22 | event: TelegramObject, 23 | data: dict[str, Any], 24 | ) -> Any: 25 | user_id = None 26 | 27 | if isinstance(event, Message): 28 | user_id = event.from_user.id 29 | elif isinstance(event, CallbackQuery): 30 | user_id = event.from_user.id 31 | 32 | if not user_id: 33 | return await handler(event, data) 34 | 35 | user_id = event.from_user.id 36 | key = f"language:{user_id}" 37 | redis: Redis = data.get("redis") 38 | language: str = await redis.get(key) or "en" 39 | _ = get_translator(language) 40 | data["_"] = _ 41 | return await handler(event, data) 42 | -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/public_url.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import TelegramObject 5 | 6 | 7 | class UrlMiddleware(BaseMiddleware): 8 | def __init__(self, url: str) -> None: 9 | self.url = url 10 | 11 | async def __call__( 12 | self, 13 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], 14 | event: TelegramObject, 15 | data: dict[str, Any], 16 | ) -> Any: 17 | data["public_url"] = self.url 18 | return await handler(event, data) 19 | -------------------------------------------------------------------------------- /backend/modules/bot/middlewares/redis.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import TelegramObject 5 | from redis.asyncio import Redis 6 | 7 | 8 | class RedisMiddleware(BaseMiddleware): 9 | def __init__(self, redis: Redis) -> None: 10 | self.redis = redis 11 | 12 | async def __call__( 13 | self, 14 | handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], 15 | event: TelegramObject, 16 | data: dict[str, Any], 17 | ) -> Any: 18 | data["redis"] = self.redis 19 | return await handler(event, data) 20 | -------------------------------------------------------------------------------- /backend/modules/bot/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from backend.modules.bot.routes.user.private import setup_private_routers 4 | 5 | def include_routers(dp: Dispatcher) -> None: 6 | private_router = setup_private_routers() 7 | dp.include_router(private_router) 8 | -------------------------------------------------------------------------------- /backend/modules/bot/routes/user/private/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router 2 | from aiogram.enums.chat_type import ChatType 3 | 4 | 5 | from backend.modules.bot.routes.user.private.messages.start import router as start 6 | from backend.modules.bot.routes.user.private.messages.start_deeplink import router as start_deeplink 7 | from backend.modules.bot.routes.user.private.messages.student_validator import router as student_validator 8 | from backend.modules.bot.routes.user.private.callback.confirmation import router as confirmation 9 | 10 | 11 | def setup_private_callback_router() -> Router: 12 | router: Router = Router(name="Private callback router") 13 | router.include_router(confirmation) 14 | return router 15 | 16 | def setup_private_message_router() -> Router: 17 | # ORDER MATTERS 18 | router: Router = Router(name="Private message router") 19 | router.include_router(start_deeplink) 20 | router.include_router(start) 21 | router.include_router(student_validator) 22 | return router 23 | 24 | def setup_private_routers(): 25 | router: Router = Router(name="Private router") 26 | router.message.filter(F.chat.type == ChatType.PRIVATE) 27 | 28 | callback_router: Router = setup_private_callback_router() 29 | message_router: Router = setup_private_message_router() 30 | router.include_router(callback_router) 31 | router.include_router(message_router) 32 | return router 33 | -------------------------------------------------------------------------------- /backend/modules/bot/routes/user/private/messages/start.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from aiogram import Router 4 | from aiogram.filters import CommandStart 5 | from aiogram.types import Message 6 | from redis.asyncio import Redis 7 | 8 | from backend.modules.bot.keyboards.kb import kb_languages, kb_url 9 | 10 | router = Router() 11 | 12 | 13 | @router.message(CommandStart(deep_link=False)) 14 | async def user_start(m: Message, public_url: str, _: Callable[[str], str], redis: Redis): 15 | await m.answer( 16 | _("Добро пожаловать в NUspace, перейди по ссылке ниже!"), 17 | reply_markup=kb_url(url=public_url), 18 | ) 19 | -------------------------------------------------------------------------------- /backend/modules/bot/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/bot/utils/__init__.py -------------------------------------------------------------------------------- /backend/modules/bot/utils/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class NotificationEnum(str, Enum): 5 | ENABLE = "enable" 6 | DISABLE = "disable" 7 | -------------------------------------------------------------------------------- /backend/modules/bot/utils/google_bucket.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from google.cloud import storage 4 | 5 | from backend.core.configs.config import config 6 | from backend.modules.google_bucket.utils import load_signing_credentials_from_info 7 | 8 | 9 | async def generate_download_url(storage_client: storage.Client, filename: str): 10 | """ 11 | Generates a signed download URL with: 12 | - 15 minute expiration 13 | - GET access only 14 | - Requires valid JWT 15 | """ 16 | # Generate signed URL using impersonated credentials to avoid private key requirement 17 | signing_credentials = load_signing_credentials_from_info(config.SIGNING_SERVICE_ACCOUNT_INFO) 18 | 19 | blob = storage_client.bucket(config.BUCKET_NAME).blob(filename) 20 | 21 | signed_url = blob.generate_signed_url( 22 | version="v4", 23 | expiration=timedelta(minutes=15), 24 | method="GET", 25 | response_type="image/jpeg", 26 | credentials=signing_credentials, 27 | ) 28 | return signed_url 29 | -------------------------------------------------------------------------------- /backend/modules/bot/utils/permissions.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ChatPermissions 2 | 3 | no_permissions = ChatPermissions( 4 | can_send_messages=False, 5 | can_send_audios=False, 6 | can_send_documents=False, 7 | can_send_photos=False, 8 | can_send_videos=False, 9 | can_send_video_notes=False, 10 | can_send_voice_notes=False, 11 | can_send_polls=False, 12 | can_invite_users=False, 13 | ) 14 | 15 | all_permissions = ChatPermissions( 16 | can_send_messages=True, 17 | can_send_audios=True, 18 | can_send_documents=True, 19 | can_send_photos=True, 20 | can_send_videos=True, 21 | can_send_video_notes=True, 22 | can_send_voice_notes=True, 23 | can_send_polls=True, 24 | can_invite_users=True, 25 | ) 26 | -------------------------------------------------------------------------------- /backend/modules/campuscurrent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/base.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from fastapi import status as http_status 3 | 4 | from backend.core.database.models.user import UserRole 5 | 6 | 7 | class BasePolicy: 8 | """Base policy with common user attributes.""" 9 | 10 | def __init__(self, user: tuple[dict, dict]): 11 | if not user or not user[0] or not user[1]: 12 | raise HTTPException( 13 | status_code=http_status.HTTP_401_UNAUTHORIZED, 14 | detail="Authentication credentials were not provided", 15 | ) 16 | self.user_creds = user 17 | self.user_role = user[1]["role"] 18 | self.user_sub = user[0]["sub"] 19 | self.communities = user[1]["communities"] 20 | self.is_admin = self.user_role == UserRole.admin.value 21 | 22 | def _is_owner(self, author_sub: str) -> bool: 23 | return self.user_sub == author_sub 24 | 25 | def _is_community_head(self, community_id: int) -> bool: 26 | return community_id in self.communities 27 | -------------------------------------------------------------------------------- /backend/modules/campuscurrent/comments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/comments/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/comments/cruds.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Type 2 | 3 | from sqlalchemy import Column, func, select 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | from sqlalchemy.orm import DeclarativeBase 6 | 7 | 8 | async def get_replies_counts( 9 | session: AsyncSession, model: Type[DeclarativeBase], parent_field: Column, parent_ids: List[int] 10 | ) -> Dict[int, int]: 11 | """ 12 | Получить количество дочерних объектов для каждого parent_id из списка parent_ids. 13 | Возвращает словарь: {parent_id: count} 14 | """ 15 | if not parent_ids: 16 | return {} 17 | stmt = ( 18 | select(parent_field, func.count(model.id)) 19 | .where(parent_field.in_(parent_ids)) 20 | .group_by(parent_field) 21 | ) 22 | result = await session.execute(stmt) 23 | return dict(result.all()) 24 | -------------------------------------------------------------------------------- /backend/modules/campuscurrent/communities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/communities/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/events/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/posts/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/posts/cruds.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sqlalchemy import func, select 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from backend.core.database.models import CommunityPost 7 | from backend.core.database.models.community import CommunityComment 8 | 9 | """exception case that QueryBuilder is unable to handle""" 10 | 11 | 12 | async def get_comment_counts(db_session: AsyncSession, posts: List[CommunityPost]) -> dict: 13 | """map post id to comment count 14 | return dict of post id and comment count""" 15 | stmt = ( 16 | select(CommunityComment.post_id, func.count(CommunityComment.id)) 17 | .where(CommunityComment.post_id.in_([post.id for post in posts])) 18 | .group_by(CommunityComment.post_id) 19 | ) 20 | result = await db_session.execute(stmt) 21 | comments_count_dict = dict(result.all()) 22 | return comments_count_dict 23 | -------------------------------------------------------------------------------- /backend/modules/campuscurrent/profile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/campuscurrent/profile/__init__.py -------------------------------------------------------------------------------- /backend/modules/campuscurrent/profile/profile.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | router = APIRouter(tags=["Profile"]) 4 | 5 | 6 | @router.get("/profile") 7 | async def get_profile(): 8 | return 9 | -------------------------------------------------------------------------------- /backend/modules/campuscurrent/tags/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | from fastapi import Query 5 | from pydantic import BaseModel, field_validator 6 | 7 | 8 | class CommunityTagRequest(BaseModel): 9 | community_id: int 10 | name: str 11 | 12 | @field_validator("name") 13 | def validate_name(cls, value: str) -> str: 14 | value = value.strip() 15 | if not value: 16 | raise ValueError("Tag name cannot be empty") 17 | if len(value) > 50: # Reasonable limit for tag names 18 | raise ValueError("Tag name cannot be longer than 50 characters") 19 | return value 20 | 21 | 22 | class BaseCommunityTag(BaseModel): 23 | id: int 24 | community_id: int 25 | name: str 26 | created_at: datetime 27 | updated_at: datetime 28 | 29 | class Config: 30 | from_attributes = True 31 | 32 | 33 | class CommunityTagResponse(BaseCommunityTag): 34 | pass 35 | 36 | 37 | class ShortCommunityTag(BaseModel): 38 | id: int 39 | name: str 40 | 41 | class Config: 42 | from_attributes = True 43 | 44 | 45 | class ListCommunityTagResponse(BaseModel): 46 | tags: List[ShortCommunityTag] = [] 47 | total_pages: int = Query(1, ge=1) 48 | -------------------------------------------------------------------------------- /backend/modules/courses/crashed/dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends 2 | 3 | from backend.common.dependencies import get_creds_or_401 4 | from backend.modules.courses.crashed.service import RegistrarService 5 | 6 | 7 | def get_registrar_service() -> RegistrarService: 8 | return RegistrarService() 9 | 10 | 11 | async def get_registrar_username( 12 | user: tuple[dict, dict] = Depends(get_creds_or_401), 13 | ) -> str: 14 | email: str = user[0]["email"] 15 | return email.split("@", maxsplit=1)[0] 16 | 17 | -------------------------------------------------------------------------------- /backend/modules/courses/statistics/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | from fastapi import Query 4 | from pydantic import BaseModel 5 | 6 | 7 | 8 | 9 | class BaseGradeReportSchema(BaseModel): 10 | id: int 11 | course_code: str 12 | course_title: str 13 | section: str 14 | term: str 15 | grades_count: int 16 | avg_gpa: float 17 | std_dev: float 18 | median_gpa: float 19 | pct_A: float 20 | pct_B: float 21 | pct_C: float 22 | pct_D: float 23 | pct_F: float 24 | pct_P: float 25 | pct_I: float 26 | pct_AU: float 27 | pct_W_AW: float 28 | letters_count: int 29 | faculty: str 30 | created_at: datetime 31 | updated_at: datetime 32 | 33 | class Config: 34 | from_attributes = True 35 | 36 | 37 | class ListGradeReportResponse(BaseModel): 38 | grades: List[BaseGradeReportSchema] = [] 39 | total_pages: int = Query(1, ge=1) 40 | 41 | -------------------------------------------------------------------------------- /backend/modules/courses/student_courses/base.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from fastapi import status as http_status 3 | 4 | from backend.core.database.models.user import UserRole 5 | 6 | 7 | class BasePolicy: 8 | """Base policy with common user attributes.""" 9 | 10 | def __init__(self, user: tuple[dict, dict]): 11 | if not user or not user[0] or not user[1]: 12 | raise HTTPException( 13 | status_code=http_status.HTTP_401_UNAUTHORIZED, 14 | detail="Authentication credentials were not provided", 15 | ) 16 | self.user_creds = user 17 | self.user_role = user[1]["role"] 18 | self.user_sub = user[0]["sub"] 19 | self.is_admin = self.user_role == UserRole.admin.value 20 | 21 | def _is_owner(self, author_sub: str) -> bool: 22 | return self.user_sub == author_sub 23 | -------------------------------------------------------------------------------- /backend/modules/courses/student_courses/dependencies.py: -------------------------------------------------------------------------------- 1 | 2 | from fastapi import Depends, HTTPException, Path, status 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | 5 | from backend.common.dependencies import get_db_session 6 | from backend.core.database.models.grade_report import CourseItem, StudentCourse 7 | 8 | 9 | async def course_item_exists_or_404( 10 | item_id: int = Path(..., description="The ID of the course item"), 11 | db_session: AsyncSession = Depends(get_db_session), 12 | ) -> CourseItem: 13 | """ 14 | Dependency to check if a course item exists. 15 | """ 16 | item = await db_session.get(CourseItem, item_id) 17 | if not item: 18 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Course item not found.") 19 | return item 20 | 21 | 22 | async def student_course_exists_or_404( 23 | student_course_id: int = Path(..., description="The ID of the student course registration"), 24 | db_session: AsyncSession = Depends(get_db_session), 25 | ) -> StudentCourse: 26 | """ 27 | Dependency to check if a student_course exists. 28 | """ 29 | student_course = await db_session.get(StudentCourse, student_course_id) 30 | if not student_course: 31 | raise HTTPException( 32 | status_code=status.HTTP_404_NOT_FOUND, detail="Student course registration not found" 33 | ) 34 | return student_course -------------------------------------------------------------------------------- /backend/modules/google_bucket/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/google_bucket/__init__.py -------------------------------------------------------------------------------- /backend/modules/google_bucket/cruds.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/google_bucket/cruds.py -------------------------------------------------------------------------------- /backend/modules/kupiprodai/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/kupiprodai/__init__.py -------------------------------------------------------------------------------- /backend/modules/notification/notification.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated, List 2 | 3 | from fastapi import APIRouter, Depends, Request 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from backend.common.cruds import QueryBuilder 7 | from backend.common.dependencies import get_creds_or_401, get_db_session 8 | from backend.core.database.models.notification import Notification 9 | from backend.modules.notification import schemas 10 | 11 | router = APIRouter(tags=["Notifications"]) 12 | 13 | 14 | @router.get("/notification", response_model=List[schemas.BaseNotification]) 15 | async def get( 16 | request: Request, 17 | user: Annotated[tuple[dict, dict], Depends(get_creds_or_401)], 18 | page: int = 1, 19 | size: int = 10, 20 | session: AsyncSession = Depends(get_db_session), 21 | ) -> List[schemas.BaseNotification]: 22 | qb: QueryBuilder = QueryBuilder(session=session, model=Notification) 23 | notifications: List[Notification] = await ( 24 | qb.base() 25 | .filter(Notification.receiver_sub == user[0]["sub"]) 26 | .paginate(page=page, size=size) 27 | .order(Notification.created_at.desc()) 28 | .all() 29 | ) 30 | return [schemas.BaseNotification.model_validate(notification) for notification in notifications] 31 | -------------------------------------------------------------------------------- /backend/modules/notification/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pydantic import BaseModel, field_validator 4 | 5 | from backend.core.database.models.common_enums import EntityType, NotificationType 6 | 7 | 8 | class RequestNotiification(BaseModel): 9 | title: str 10 | message: str 11 | notification_source: EntityType 12 | receiver_sub: str 13 | telegram_id: int | None = None 14 | type: NotificationType 15 | url: str | None = None 16 | 17 | @field_validator("url") 18 | def validate_url(cls, v): 19 | if isinstance(v, str) and (not v.startswith("https://")): 20 | raise ValueError("URL must start with https://") 21 | return v 22 | 23 | 24 | class BaseNotification(BaseModel): 25 | id: int 26 | title: str 27 | message: str 28 | notification_source: EntityType 29 | receiver_sub: str 30 | tg_id: int 31 | type: NotificationType 32 | url: str | None = None 33 | created_at: datetime.datetime 34 | 35 | class Config: 36 | from_attributes = True 37 | 38 | 39 | class _RequestNotification(BaseNotification): 40 | switch: bool 41 | -------------------------------------------------------------------------------- /backend/modules/review/service.py: -------------------------------------------------------------------------------- 1 | from backend.core.database.models import Community, Event, Product, User 2 | from backend.core.database.models.review import OwnerType, ReviewableType 3 | 4 | REVIEWABLE_TYPE_MODEL_MAP = { 5 | ReviewableType.products: Product, 6 | ReviewableType.club_events: Event, 7 | } 8 | 9 | REVIEWABLE_TYPE_PARENT_MODEL_MAP = { 10 | OwnerType.users: User, 11 | OwnerType.clubs: Community, 12 | } 13 | 14 | SECOND_REVIEWABLE_TYPE_PARENT_MODEL_MAP = { 15 | Product: User, 16 | Event: Community, 17 | } 18 | 19 | # Mapping for primary key field names 20 | MODEL_PRIMARY_KEY_MAP = { 21 | User: "sub", 22 | Community: "id", 23 | Product: "id", 24 | Event: "id", 25 | } 26 | -------------------------------------------------------------------------------- /backend/modules/review/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from backend.common.schemas import MediaResponse 4 | from backend.core.database.models.review import Review 5 | from backend.modules.review.schemas import ReviewResponseSchema 6 | 7 | 8 | def build_review_response(review: Review, media: List[MediaResponse]) -> ReviewResponseSchema: 9 | return ReviewResponseSchema( 10 | id=review.id, 11 | reviewable_type=review.reviewable_type, 12 | entity_id=review.entity_id, 13 | user_sub=review.user_sub, 14 | rating=review.rating, 15 | content=review.content, 16 | owner_type=review.owner_type, 17 | owner_id=review.owner_id, 18 | created_at=review.created_at, 19 | updated_at=review.updated_at, 20 | media=media, 21 | reply=review.reply, 22 | ) 23 | -------------------------------------------------------------------------------- /backend/modules/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/search/__init__.py -------------------------------------------------------------------------------- /backend/modules/search/search.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Query, Request 2 | from httpx import HTTPError 3 | 4 | from backend.common.utils import meilisearch 5 | from backend.core.database.models.common_enums import EntityType 6 | from backend.core.database.models.product import ProductStatus 7 | 8 | router = APIRouter(tags=["Search Routes"]) 9 | 10 | 11 | @router.get("/search/") 12 | async def full_search( 13 | request: Request, 14 | keyword: str, 15 | storage_name: EntityType, 16 | page: int = 1, 17 | size: int = Query(10, ge=1, le=30), 18 | ): 19 | """ 20 | Full search implementation returning complete entity details 21 | """ 22 | filters = ( 23 | [f"status = {ProductStatus.active.value}"] if storage_name == EntityType.products else None 24 | ) 25 | print(storage_name.value) 26 | try: 27 | result = await meilisearch.get( 28 | client=request.app.state.meilisearch_client, 29 | storage_name=storage_name.value, 30 | keyword=keyword, 31 | page=page, 32 | size=size, 33 | filters=filters, 34 | ) 35 | print(result) 36 | return result["hits"] # Return full entity details 37 | except HTTPError: 38 | # Error handling similar to pre_search 39 | pass 40 | -------------------------------------------------------------------------------- /backend/modules/search/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/backend/modules/search/utils.py -------------------------------------------------------------------------------- /backend/modules/sgotinish/base.py: -------------------------------------------------------------------------------- 1 | from backend.core.database.models.user import UserRole 2 | 3 | 4 | from fastapi import HTTPException 5 | from fastapi import status as http_status 6 | 7 | 8 | class BasePolicy: 9 | """Base policy with common user attributes.""" 10 | 11 | def __init__(self, user_creds: tuple[dict, dict]): 12 | if not user_creds or not user_creds[0] or not user_creds[1]: 13 | raise HTTPException( 14 | status_code=http_status.HTTP_401_UNAUTHORIZED, 15 | detail="Authentication credentials were not provided", 16 | ) 17 | self.user_creds = user_creds 18 | self.user_role = user_creds[1]["role"] 19 | self.user_sub = user_creds[0]["sub"] 20 | self.department_id = user_creds[1].get("department_id") 21 | self.is_admin = self.user_role == UserRole.admin.value 22 | self.is_sg_member = self.user_role in [ 23 | UserRole.boss.value, 24 | UserRole.capo.value, 25 | UserRole.soldier.value, 26 | ] 27 | 28 | def _is_owner(self, author_sub: str) -> bool: 29 | return self.user_sub == author_sub 30 | -------------------------------------------------------------------------------- /backend/modules/sgotinish/messages/dependencies.py: -------------------------------------------------------------------------------- 1 | from backend.core.database.models.sgotinish import Message 2 | from backend.common.cruds import QueryBuilder 3 | from backend.common.dependencies import get_db_session 4 | from fastapi import Depends, HTTPException, status 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | from sqlalchemy.orm import selectinload 7 | from backend.core.database.models.sgotinish import Conversation 8 | 9 | 10 | 11 | async def message_exists_or_404( 12 | message_id: int, 13 | db_session: AsyncSession = Depends(get_db_session), 14 | ) -> Message: 15 | """ 16 | Dependency to validate that a message exists and return it. 17 | 18 | Args: 19 | message_id: ID of the message to validate 20 | db_session: Database session 21 | 22 | Returns: 23 | Message: The message if found 24 | 25 | Raises: 26 | HTTPException: 404 if message not found 27 | """ 28 | qb = QueryBuilder(session=db_session, model=Message) 29 | message = ( 30 | await qb.base() 31 | .eager(Message.conversation, Message.sender) 32 | .option(selectinload(Message.conversation).selectinload(Conversation.ticket)) 33 | .filter(Message.id == message_id) 34 | .first() 35 | ) 36 | if not message: 37 | raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Message not found") 38 | return message 39 | 40 | -------------------------------------------------------------------------------- /backend/modules/sgotinish/tickets/interfaces.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | 4 | from backend.core.database.models.sgotinish import Ticket, TicketAccess, Message 5 | from backend.core.database.models.user import User 6 | from backend.modules.sgotinish.tickets import schemas 7 | 8 | 9 | class AbstractNotificationService(ABC): 10 | @abstractmethod 11 | async def notify_new_ticket_to_bosses(self, ticket: Ticket, bosses: List[User]) -> None: 12 | """Notifies bosses about a new ticket.""" 13 | pass 14 | 15 | async def notify_ticket_access_granted(self, ticket: Ticket, access: TicketAccess) -> None: 16 | """Notifies user about a ticket access granted.""" 17 | pass 18 | 19 | @abstractmethod 20 | async def notify_ticket_updated(self, ticket: Ticket) -> None: 21 | """Notifies user about a ticket updated.""" 22 | pass 23 | 24 | @abstractmethod 25 | async def notify_new_message(self, message: Message) -> None: 26 | """Notifies user about a new message.""" 27 | pass 28 | 29 | class AbstractConversationService(ABC): 30 | @abstractmethod 31 | async def get_conversation_dtos_for_tickets( 32 | self, tickets: List[Ticket], user: tuple[dict, dict] 33 | ) -> dict[int, List[schemas.ConversationResponseDTO]]: 34 | """Gets conversation DTOs for a list of tickets.""" 35 | pass 36 | 37 | -------------------------------------------------------------------------------- /backend/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ "$IS_DEBUG" = "false" ]; then 3 | gunicorn -w $(( $(nproc) * 2 + 1 )) -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000 backend.main:app; 4 | else 5 | uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload; 6 | fi 7 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test package for the backend application. 3 | """ 4 | -------------------------------------------------------------------------------- /backend/tests/routes/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test package for route-specific tests. 3 | """ 4 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODOs 2 | ## Current 3 | - Telegram bot for PR: 4 | Assigned: @mciiee 5 | 6 | - Backend architecture rewrite. 7 | Assigned: NO 8 | 9 | ## On hold 10 | -------------------------------------------------------------------------------- /frontend/Dockerfile_static_builder: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | WORKDIR /app 3 | 4 | # Copy package files 5 | COPY frontend/package*.json ./ 6 | 7 | # Install dependencies 8 | RUN npm install 9 | 10 | # Copy source code (build is done in the container command) 11 | COPY frontend . -------------------------------------------------------------------------------- /frontend/Dockerfile_vite: -------------------------------------------------------------------------------- 1 | # Use official Node.js image with Alpine Linux 2 | FROM node:20-alpine 3 | 4 | # Install bash and other essential tools for development 5 | RUN apk add --no-cache bash curl git jq 6 | 7 | # Set working directory in container 8 | WORKDIR /nuros 9 | 10 | # Copy only package.json and package-lock.json for caching dependencies 11 | COPY frontend/package*.json ./ 12 | # COPY frontend/package-lock.json ./ 13 | 14 | # Clean install dependencies (force rebuild for container architecture) 15 | RUN npm install 16 | 17 | # Copy the rest of the project files 18 | COPY frontend . 19 | 20 | # Script to resolve quick tunnel URL from cloudflared metrics and export for Vite 21 | RUN printf '%s\n' '#!/bin/sh' \ 22 | 'set -e' \ 23 | 'cd /nuros' \ 24 | 'METRICS="http://cloudflared:2000/metrics"' \ 25 | 'for i in 1 2 3 4 5; do' \ 26 | ' URL=$(curl -fsS "$METRICS" | grep -Eo "https://[a-z0-9-]+\\.trycloudflare\\.com" | head -n1 || true)' \ 27 | ' if [ -n "$URL" ]; then echo "$URL"; break; fi' \ 28 | ' sleep 2' \ 29 | 'done' \ 30 | 'if [ -z "$URL" ]; then URL="http://localhost:5173"; fi' \ 31 | 'export CLOUDFLARED_TUNNEL_URL="$URL"' \ 32 | 'echo "Using CLOUDFLARED_TUNNEL_URL=$CLOUDFLARED_TUNNEL_URL"' \ 33 | 'exec npm run dev -- --host' > /usr/local/bin/start-frontend.sh \ 34 | && chmod +x /usr/local/bin/start-frontend.sh 35 | 36 | # Start the Vite development server 37 | CMD ["/usr/local/bin/start-frontend.sh"] 38 | -------------------------------------------------------------------------------- /frontend/src/app/main.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClientProvider } from "@tanstack/react-query"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import App from "./App"; 7 | import { BackNavigationProvider } from "@/context/BackNavigationContext"; 8 | import { ThemeProvider } from "../context/ThemeProviderContext"; 9 | import "./index.css"; 10 | import { queryClient } from "../utils/query-client"; 11 | 12 | ReactDOM.createRoot(document.getElementById("root")!).render( 13 | 14 | 15 | 16 | 17 | {/* Provider is already used inside AppsLayout where needed; keep here for any global consumers */} 18 | 19 | 20 | 21 | 22 | 23 | 24 | , 25 | ); 26 | -------------------------------------------------------------------------------- /frontend/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-icon-180.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1125-2436.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1125-2436.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1136-640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1136-640.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1170-2532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1170-2532.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1179-2556.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1179-2556.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1206-2622.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1206-2622.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1242-2208.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1242-2208.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1242-2688.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1242-2688.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1260-2736.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1260-2736.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1284-2778.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1284-2778.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1290-2796.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1290-2796.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1320-2868.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1320-2868.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1334-750.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1334-750.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1488-2266.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1488-2266.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1536-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1536-2048.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1620-2160.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1620-2160.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1640-2360.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1640-2360.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1668-2224.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1668-2224.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1668-2388.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1668-2388.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-1792-828.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-1792-828.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2048-1536.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2048-1536.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2048-2732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2048-2732.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2160-1620.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2160-1620.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2208-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2208-1242.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2224-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2224-1668.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2266-1488.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2266-1488.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2360-1640.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2360-1640.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2388-1668.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2388-1668.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2436-1125.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2436-1125.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2532-1170.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2532-1170.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2556-1179.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2556-1179.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2622-1206.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2622-1206.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2688-1242.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2688-1242.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2732-2048.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2732-2048.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2736-1260.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2736-1260.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2778-1284.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2778-1284.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2796-1290.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2796-1290.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-2868-1320.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-2868-1320.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-640-1136.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-640-1136.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-750-1334.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-750-1334.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/apple-splash-828-1792.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/apple-splash-828-1792.jpg -------------------------------------------------------------------------------- /frontend/src/assets/icons/manifest-icon-192.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/manifest-icon-192.maskable.png -------------------------------------------------------------------------------- /frontend/src/assets/icons/manifest-icon-512.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/icons/manifest-icon-512.maskable.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/all.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/appliances.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/appliances.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/books.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/books.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/clothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/clothing.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/electronics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/electronics.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/food.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/food.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/furniture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/furniture.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/others.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/others.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/sports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/sports.png -------------------------------------------------------------------------------- /frontend/src/assets/images/categories/transport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/categories/transport.png -------------------------------------------------------------------------------- /frontend/src/assets/images/google_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/google_form.png -------------------------------------------------------------------------------- /frontend/src/assets/images/hero_assets/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/hero_assets/1.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/hero_assets/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/hero_assets/2.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/hero_assets/3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/hero_assets/3.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/hero_assets/4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/hero_assets/4.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/hero_assets/5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/hero_assets/5.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/miniapp-resized.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/miniapp-resized.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/miniapp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/miniapp.webp -------------------------------------------------------------------------------- /frontend/src/assets/images/nu-space-presentation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/nu-space-presentation.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/adil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/adil.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/aisana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/aisana.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/alan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/alan.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/bakhtiyar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/bakhtiyar.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/ulan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/ulan.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/teams/yelnur.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/teams/yelnur.jpg -------------------------------------------------------------------------------- /frontend/src/assets/images/welcome-nu-space.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulanpy/nuspace/9f3a999ecdf70c1ab2a5285c4d477bc7df0c3530/frontend/src/assets/images/welcome-nu-space.jpg -------------------------------------------------------------------------------- /frontend/src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuspace", 3 | "shortname": "nuspace", 4 | "start_url": "/", 5 | "icons": [ 6 | { 7 | "src": "/src/assets/icons/manifest-icon-192.maskable.png", 8 | "sizes": "192x192", 9 | "type": "image/png", 10 | "purpose": "any" 11 | }, 12 | { 13 | "src": "/src/assets/icons/manifest-icon-192.maskable.png", 14 | "sizes": "192x192", 15 | "type": "image/png", 16 | "purpose": "maskable" 17 | }, 18 | { 19 | "src": "/src/assets/icons/manifest-icon-512.maskable.png", 20 | "sizes": "512x512", 21 | "type": "image/png", 22 | "purpose": "any" 23 | }, 24 | { 25 | "src": "/src/assets/icons/manifest-icon-512.maskable.png", 26 | "sizes": "512x512", 27 | "type": "image/png", 28 | "purpose": "maskable" 29 | } 30 | ], 31 | "theme_color": "#000000", 32 | "background_color": "#FFFFFF", 33 | "display": "fullscreen", 34 | "orientation": "portrait" 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/Vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/assets/svg/telegram-connected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/components/animations/AnimatedCard.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import { Card } from "../atoms/card"; 3 | import { ReactNode } from "react"; 4 | 5 | interface AnimatedCardProps { 6 | children: ReactNode; 7 | hasFloatingBackground?: boolean; 8 | backgroundEffects?: ReactNode; 9 | variants?: any; 10 | className?: string; 11 | [key: string]: any; 12 | } 13 | 14 | export function AnimatedCard({ 15 | children, 16 | hasFloatingBackground = false, 17 | backgroundEffects, 18 | variants, 19 | className = "", 20 | ...props 21 | }: AnimatedCardProps) { 22 | return ( 23 | 24 | 28 | {hasFloatingBackground && ( 29 |
30 | {backgroundEffects} 31 |
32 | )} 33 |
34 | {children} 35 |
36 |
37 |
38 | ); 39 | } -------------------------------------------------------------------------------- /frontend/src/components/atoms/badge.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "../../utils/utils"; 4 | 5 | const badgeVariants = cva( 6 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 12 | secondary: 13 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 14 | destructive: 15 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 16 | outline: "text-foreground", 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: "default", 21 | }, 22 | }, 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return ( 31 |
32 | ); 33 | } 34 | 35 | export { Badge, badgeVariants }; 36 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "../../utils/utils"; 3 | 4 | export interface InputProps 5 | extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/utils/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 3 | 4 | import { cn } from "@/utils/utils"; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverTrigger, PopoverContent }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "../../utils/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )); 24 | Progress.displayName = ProgressPrimitive.Root.displayName; 25 | 26 | export { Progress }; 27 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/slider-button.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeft, ChevronRight } from "lucide-react"; 2 | import { useTheme } from "../../context/ThemeProviderContext"; 3 | 4 | interface SliderButtonProps { 5 | direction: "left" | "right"; 6 | onClick: () => void; 7 | } 8 | 9 | export const SliderButton = ({ direction, onClick }: SliderButtonProps) => { 10 | const { theme } = useTheme(); 11 | const isDarkTheme = theme === "dark"; 12 | 13 | return ( 14 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { Toaster as Sonner, toast } from "sonner"; 3 | 4 | type ToasterProps = React.ComponentProps; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | 26 | ); 27 | }; 28 | 29 | export { Toaster, toast }; 30 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/spinner.tsx: -------------------------------------------------------------------------------- 1 | export const Spinner = () => { 2 | return ( 3 |
4 |
5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | import { cn } from "../../utils/utils"; 4 | 5 | const Switch = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /frontend/src/components/atoms/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/utils/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |