Merge branch 'backend-rewrite' into 'main'

Rewrote this piece of shit backend

See merge request root/fileserver!7
This commit is contained in:
Mutzi 2022-08-28 17:07:30 +00:00
commit 806563de4d
111 changed files with 7256 additions and 9327 deletions

View File

@ -1,29 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin', 'no-relative-import-paths'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ 'allowSameFolder': true, 'rootDir': 'src' }
]
},
};

422
.gitignore vendored
View File

@ -1,37 +1,58 @@
# Created by .ignore support plugin (hsz.mobi) # Created by https://www.toptal.com/developers/gitignore/api/clion
### JetBrains template # Edit at https://www.toptal.com/developers/gitignore?templates=clion
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
### CLion ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff: # User-specific stuff
.idea/**/workspace.xml .idea/**/workspace.xml
.idea/**/tasks.xml .idea/**/tasks.xml
.idea/dictionaries .idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files: # AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/ .idea/**/dataSources/
.idea/**/dataSources.ids .idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml .idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml .idea/**/sqlDataSources.xml
.idea/**/dynamic.xml .idea/**/dynamic.xml
.idea/**/uiDesigner.xml .idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle: # Gradle
.idea/**/gradle.xml .idea/**/gradle.xml
.idea/**/libraries .idea/**/libraries
# CMake # Gradle and Maven with auto-import
cmake-build-debug/ # When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# Mongo Explorer plugin: # CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml .idea/**/mongoSettings.xml
## File-based project format: # File-based project format
*.iws *.iws
## Plugin-specific files:
# IntelliJ # IntelliJ
out/ out/
@ -44,358 +65,55 @@ atlassian-ide-plugin.xml
# Cursive Clojure plugin # Cursive Clojure plugin
.idea/replstate.xml .idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ) # Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
### VisualStudio template
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files # Editor-based Rest Client
*.suo .idea/httpRequests
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio) # Android studio 3.1+ serialized cache file
*.userprefs .idea/caches/build_file_checksums.ser
# Build results ### CLion Patch ###
[Dd]ebug/ # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory # *.iml
.vs/ # modules.xml
# Uncomment if you have tasks that create the project's static files in wwwroot # .idea/misc.xml
#wwwroot/ # *.ipr
# MSTest test Results # Sonarlint plugin
[Tt]est[Rr]esult*/ # https://plugins.jetbrains.com/plugin/7973-sonarlint
[Bb]uild[Ll]og.* .idea/**/sonarlint/
# NUNIT # SonarQube Plugin
*.VisualState.xml # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
TestResult.xml .idea/**/sonarIssues.xml
# Build Results of an ATL Project # Markdown Navigator plugin
[Dd]ebugPS/ # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
[Rr]eleasePS/ .idea/**/markdown-navigator.xml
dlldata.c .idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Benchmark Results # Cache file creation bug
BenchmarkDotNet.Artifacts/ # See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# .NET Core # CodeStream plugin
project.lock.json # https://plugins.jetbrains.com/plugin/12206-codestream
project.fragment.lock.json .idea/codestream.xml
artifacts/
**/Properties/launchSettings.json
*_i.c # Azure Toolkit for IntelliJ plugin
*_p.c # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
*_i.h .idea/**/azureSettings.xml
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files # End of https://www.toptal.com/developers/gitignore/api/clion
_Chutzpah*
# Visual C++ cache files run/
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
coverage/
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
=======
# Local
.env
dist
files
sqlite.db

View File

@ -1,89 +1,42 @@
image: node:latest image: ubuntu:latest
stages: stages:
- setup
- test
- build - build
- package - package
cache: &global_cache
paths:
- .yarn
- node_modules
- frontend/.yarn
- frontend/node_modules
policy: pull
before_script:
- yarn install --cache-folder .yarn --frozen-lockfile
- cd frontend
- yarn install --cache-folder .yarn --frozen-lockfile
- cd ..
.dto_artifacts_need: &dto_artifacts_need
job: test_build_dto
artifacts: true
test_build_dto:
stage: setup
cache:
<<: *global_cache
policy: pull-push
before_script: []
script:
- cd dto
- yarn install --frozen-lockfile
- yarn lint
- yarn build
- cd ..
- yarn install --cache-folder .yarn --frozen-lockfile
- yarn add ./dto
- cd frontend
- yarn install --cache-folder .yarn --frozen-lockfile
- yarn add ../dto
artifacts:
paths:
- dto/lib/
test_backend:
needs:
- *dto_artifacts_need
stage: test
script:
- yarn lint
test_frontend:
needs:
- *dto_artifacts_need
stage: test
script:
- cd frontend
- yarn lint
build_backend: build_backend:
stage: build stage: build
needs: cache:
- *dto_artifacts_need paths:
- job: test_backend - /root/.cache/vcpkg
artifacts: false
script: script:
- echo This has to work till I rewrite the backend - apt-get update
- false && echo - apt-get install g++ gcc make cmake git curl zip unzip tar python3 pkg-config -y
- yarn webpack - SRC="$PWD"
- TMP=$(mktemp -d)
- cd $TMP
- git clone https://github.com/Microsoft/vcpkg.git .
- ./bootstrap-vcpkg.sh -disableMetrics
- cd $SRC
- cmake -B build -S backend -DCMAKE_TOOLCHAIN_FILE=$TMP/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release
- cmake --build build
- cp build/backend server
artifacts: artifacts:
paths: paths:
- dist/ - server
expire_in: 1h expire_in: 1h
build_frontend: test_and_build_frontend:
image: node:latest
stage: build stage: build
needs: cache:
- *dto_artifacts_need paths:
- job: test_frontend - frontend/.yarn
artifacts: false - frontend/node_modules
script: script:
- cd frontend - cd frontend
- yarn install --cache-folder .yarn --frozen-lockfile
- yarn lint
- yarn build - yarn build
artifacts: artifacts:
paths: paths:
@ -92,23 +45,16 @@ build_frontend:
package_server: package_server:
stage: package stage: package
cache: []
before_script: [] before_script: []
needs: needs:
- job: build_backend - job: build_backend
artifacts: true artifacts: true
- job: build_frontend - job: test_and_build_frontend
artifacts: true artifacts: true
script: script:
- TMP=$(mktemp -d) - mkdir static
- mv dist/* "$TMP" - mv frontend/dist/* static/
- mkdir "$TMP/frontend"
- mv frontend/dist/* "$TMP/frontend"
- rm -r *
- rm -r .* || true
- mv "$TMP/"* .
artifacts: artifacts:
paths: paths:
- package.json - server
- server.js - static/
- frontend/

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
backend

19
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="sqlite.db" uuid="6e8086dd-b853-422e-b48a-7c96a2403352">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/old_backend/sqlite.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="sqlite.db [2]" uuid="788293bd-abec-4b6b-a13e-26da21cb36dd">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/run/sqlite.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

2
.idea/file_server.iml Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module classpath="CMake" type="CPP_MODULE" version="4" />

11
.idea/misc.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$/backend">
<contentRoot DIR="$PROJECT_DIR$" />
</component>
<component name="CidrRootsConfiguration">
<libraryRoots>
<file path="$PROJECT_DIR$/backend/lib" />
</libraryRoots>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/file_server.iml" filepath="$PROJECT_DIR$/.idea/file_server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,7 +0,0 @@
{
"tabWidth": 4,
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"endOfLine": "lf"
}

View File

@ -1,19 +0,0 @@
# Mutzi's fileserver
## Description
The most crackhead fileserver you will find on the market
## Installation
```bash
npm install
cd frontend && npm install
```
## Running the app
```bash
npm run start:dev
```
Run in parallel for building the frontend:
````bash
cd frontend && npm run serve
````

70
backend/CMakeLists.txt Normal file
View File

@ -0,0 +1,70 @@
cmake_minimum_required(VERSION 3.20)
project(backend)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
add_executable(backend
src/main.cpp
src/dto/dto.h
src/dto/responses.cpp
src/db/db.h
src/db/db.cpp
src/db/model/Inode.cc
src/db/model/Inode.h
src/db/model/Tokens.cc
src/db/model/Tokens.h
src/db/model/User.cc
src/db/model/User.h
src/controllers/controllers.h
src/controllers/admin.cpp
src/controllers/fs.cpp
src/controllers/user.cpp
src/controllers/auth/auth_common.cpp
src/controllers/auth/auth_basic.cpp
src/controllers/auth/auth_2fa.cpp
src/controllers/auth/auth_gitlab.cpp
src/filters/filters.h
src/filters/filters.cpp
)
find_package(Drogon CONFIG REQUIRED)
find_package(CURL CONFIG REQUIRED)
find_package(PNG REQUIRED)
find_path(JWT_CPP_INCLUDE_DIRS "jwt-cpp/base.h")
find_path(BOTAN_INCLUDE_DIRS "botan/botan.h")
find_path(QR_INCLUDE_DIRS "qrcodegen.hpp")
find_path(PNGPP_INCLUDE_DIRS "png++/color.hpp")
find_library(BOTAN_LIBRARY botan-2)
find_library(QR_LIBRARY nayuki-qr-code-generator)
target_include_directories(backend PRIVATE
src
${JWT_CPP_INCLUDE_DIRS}
${BOTAN_INCLUDE_DIRS}
${QR_INCLUDE_DIRS}
${PNGPP_INCLUDE_DIRS}
)
target_link_libraries(backend
Drogon::Drogon
CURL::libcurl
PNG::PNG
${BOTAN_LIBRARY}
${QR_LIBRARY}
)
install(TARGETS backend)
target_compile_options(backend PRIVATE
$<$<CONFIG:Debug>:-g -Wall -Wno-unknown-pragmas>
$<$<CONFIG:Release>:-O3>
)

View File

@ -0,0 +1,88 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include "controllers.h"
#include "dto/dto.h"
namespace api {
void admin::users(req_type, cbk_type cbk) {
db::MapperUser user_mapper(drogon::app().getDbClient());
std::vector<dto::Responses::GetUsersEntry> entries;
auto users = user_mapper.findAll();
for (const db::User& user : users)
entries.emplace_back(
user.getValueOfId(),
user.getValueOfGitlab() != 0,
db::User_getEnumTfaType(user) != db::tfaTypes::NONE,
user.getValueOfName(),
db::User_getEnumRole(user)
);
cbk(dto::Responses::get_admin_users_res(entries));
}
void admin::set_role(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::UserRole role = (db::UserRole)dto::json_get<int>(json, "role").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
user.setRole(role);
user_mapper.update(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::logout(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
auth::revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::delete_user(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
auth::revoke_all(user);
fs::delete_node(fs::get_node(user.getValueOfRootId()).value(), true);
user_mapper.deleteOne(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void admin::disable_2fa(req_type req, cbk_type cbk) {
Json::Value& json = *req->jsonObject();
try {
uint64_t user_id = dto::json_get<uint64_t>(json, "user").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto user = user_mapper.findByPrimaryKey(user_id);
user.setTfaType(db::tfaTypes::NONE);
user_mapper.update(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,103 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <botan/base32.h>
#include <botan/base64.h>
#include <qrcodegen.hpp>
#include <png++/png.hpp>
#include "controllers/controllers.h"
#include "db/db.h"
#include "dto/dto.h"
std::string create_totp_qrcode(const db::User& user, const std::string& b32_secret) {
const int qrcode_pixel_size = 4;
std::stringstream code_ss;
code_ss << "otpauth://totp/MFileserver:"
<< user.getValueOfName()
<< "?secret="
<< b32_secret
<< "&issuer=MFileserver";
auto code = qrcodegen::QrCode::encodeText(code_ss.str().c_str(), qrcodegen::QrCode::Ecc::MEDIUM);
const int mod_count = code.getSize();
png::image<png::gray_pixel> image(mod_count*qrcode_pixel_size, mod_count*qrcode_pixel_size);
for (int x = 0; x < mod_count; x++) for (int y = 0; y < mod_count; y++) {
const bool mod = code.getModule(x, y);
const int x_img_start = x * qrcode_pixel_size, y_img_start = y * qrcode_pixel_size;
for (int x_img = x_img_start; x_img < x_img_start + qrcode_pixel_size; x_img++) for (int y_img = y_img_start; y_img < y_img_start + qrcode_pixel_size; y_img++)
image[x_img][y_img] = mod ? 0 : 0xff;
}
std::stringstream image_ss;
image.write_stream(image_ss);
std::string image_str = image_ss.str();
std::vector<uint8_t> secret(image_str.data(), image_str.data()+image_str.size());
return "data:image/png;base64," + Botan::base64_encode(secret);
}
namespace api {
void auth::tfa_setup(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
bool mail = dto::json_get<bool>(json, "mail").value();
auto secret_uchar = rng->random_vec(32);
std::vector<char> secret(secret_uchar.data(), secret_uchar.data()+32);
user.setTfaSecret(secret);
db::MapperUser user_mapper(drogon::app().getDbClient());
user_mapper.update(user);
if (mail) {
send_mail(user);
cbk(dto::Responses::get_success_res());
} else {
std::string b32_secret = Botan::base32_encode(secret_uchar);
b32_secret.erase(std::remove(b32_secret.begin(), b32_secret.end(), '='), b32_secret.end());
std::string code = create_totp_qrcode(user, b32_secret);
cbk(dto::Responses::get_tfa_setup_res(b32_secret, code));
}
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::tfa_complete(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
bool mail = dto::json_get<bool>(json, "mail").value();
uint32_t code = std::stoi(dto::json_get<std::string>(json, "code").value());
user.setTfaType(mail ? db::tfaTypes::EMAIL : db::tfaTypes::TOTP);
if (!verify2fa(user, code))
return cbk(dto::Responses::get_unauth_res("Incorrect 2fa"));
db::MapperUser user_mapper(drogon::app().getDbClient());
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::tfa_disable(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
db::MapperUser user_mapper(drogon::app().getDbClient());
user.setTfaType(db::tfaTypes::NONE);
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,135 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <botan/argon2.h>
#include <botan/totp.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include "controllers/controllers.h"
#include "db/db.h"
#include "dto/dto.h"
namespace api {
void auth::login(req_type req, cbk_type cbk) {
Json::Value &json = *req->jsonObject();
try {
std::string username = dto::json_get<std::string>(json, "username").value();
std::string password = dto::json_get<std::string>(json, "password").value();
std::optional<std::string> otp = dto::json_get<std::string>(json, "otp");
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
auto db_users = user_mapper.findBy(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, username) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 0)
);
if (db_users.empty()) {
cbk(dto::Responses::get_unauth_res("Invalid username or password"));
return;
}
db::User &db_user = db_users.at(0);
if (!Botan::argon2_check_pwhash(password.c_str(), password.size(), db_user.getValueOfPassword())) {
cbk(dto::Responses::get_unauth_res("Invalid username or password"));
return;
}
if (db::User_getEnumRole(db_user) == db::UserRole::DISABLED) {
cbk(dto::Responses::get_unauth_res("Account is disabled"));
return;
}
const auto tfa = db::User_getEnumTfaType(db_user);
if (tfa != db::tfaTypes::NONE) {
if (!otp.has_value()) {
if (tfa == db::tfaTypes::EMAIL) send_mail(db_user);
return cbk(dto::Responses::get_success_res());
}
if (!verify2fa(db_user, std::stoi(otp.value())))
return cbk(dto::Responses::get_unauth_res("Incorrect 2fa"));
}
cbk(dto::Responses::get_login_res(get_token(db_user)));
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::signup(req_type req, cbk_type cbk) {
Json::Value &json = *req->jsonObject();
try {
std::string username = dto::json_get<std::string>(json, "username").value();
std::string password = dto::json_get<std::string>(json, "password").value();
db::MapperUser user_mapper(drogon::app().getDbClient());
auto existing_users = user_mapper.count(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, username) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 0)
);
if (existing_users != 0) {
cbk(dto::Responses::get_badreq_res("Username is already taken"));
return;
}
//std::string hash = Botan::argon2_generate_pwhash(password.c_str(), password.size(), *rng, 1, 256*1024, 2);
std::string hash = Botan::argon2_generate_pwhash(password.c_str(), password.size(), *rng, 1, 16*1024, 1);
db::User new_user;
new_user.setName(username);
new_user.setPassword(hash);
new_user.setGitlab(0);
new_user.setRole(db::UserRole::DISABLED);
new_user.setRootId(0);
new_user.setTfaType(db::tfaTypes::NONE);
user_mapper.insert(new_user);
generate_root(new_user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception& e) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void auth::refresh(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
db::Token token = dto::get_token(req);
db::MapperToken token_mapper(drogon::app().getDbClient());
token_mapper.deleteOne(token);
cbk(dto::Responses::get_login_res( get_token(user)));
}
void auth::logout_all(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
revoke_all(user);
cbk(dto::Responses::get_success_res());
}
void auth::change_password(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value &json = *req->jsonObject();
try {
std::string old_pw = dto::json_get<std::string>(json, "oldPassword").value();
std::string new_pw = dto::json_get<std::string>(json, "newPassword").value();
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
if (!Botan::argon2_check_pwhash(old_pw.c_str(), old_pw.size(), user.getValueOfPassword()))
return cbk(dto::Responses::get_unauth_res("Old password is wrong"));
std::string hash = Botan::argon2_generate_pwhash(new_pw.c_str(), new_pw.size(), *rng, 1, 256*1024, 2);
user.setPassword(hash);
user_mapper.update(user);
revoke_all(user);
cbk(dto::Responses::get_success_res());
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,110 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <chrono>
#include <iomanip>
#include <botan/argon2.h>
#include <botan/uuid.h>
#include <botan/totp.h>
#if defined(BOTAN_HAS_SYSTEM_RNG)
#include <botan/system_rng.h>
#else
#include <botan/auto_rng.h>
#endif
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include <curl/curl.h>
#include "controllers/controllers.h"
#include "db/db.h"
#include "dto/dto.h"
size_t payload_source(char* ptr, size_t size, size_t nmemb, void* userp) {
auto* ss = (std::stringstream*)userp;
return ss->readsome(ptr, (long)(size*nmemb));
}
namespace api {
#if defined(BOTAN_HAS_SYSTEM_RNG)
std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::System_RNG>();
#else
std::unique_ptr<Botan::RNG> auth::rng = std::make_unique<Botan::AutoSeeded_RNG>();
#endif
bool auth::verify2fa(const db::User& user, uint32_t totp) {
size_t allowed_skew = db::User_getEnumTfaType(user) == db::tfaTypes::TOTP ? 0 : 10;
const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
return Botan::TOTP(Botan::OctetString(totp_secret)).verify_totp(totp, std::chrono::system_clock::now(), allowed_skew);
}
void auth::send_mail(const db::User& user) {
std::stringstream ss;
std::time_t t = std::time(nullptr);
const auto& totp_secret = (const std::vector<uint8_t>&) user.getValueOfTfaSecret();
char totp[16];
std::snprintf(totp, 16, "%06d", Botan::TOTP(Botan::OctetString(totp_secret)).generate_totp(t));
ss.imbue(std::locale("en_US.utf8"));
ss << "Date: " << std::put_time(std::localtime(&t), "%a, %d %b %Y %T %z") << "\r\n";
ss << "To: " << user.getValueOfName() << "\r\n";
ss << "From: fileserver@mattv.de\r\n";
ss << "Message-ID: " << Botan::UUID(*rng).to_string() << "@mattv.de>\r\n";
ss << "Subject: Fileserver - EMail 2fa code\r\n";
ss << "Your code is: " << totp << "\r\n";
ss << "It is valid for 5 Minutes\r\n";
CURL* curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_USERNAME, "no-reply@mattv.de");
curl_easy_setopt(curl, CURLOPT_PASSWORD, "noreplyLONGPASS123");
curl_easy_setopt(curl, CURLOPT_URL, "smtp://mail.mattv.de:587");
curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
auto recp = curl_slist_append(nullptr, user.getValueOfName().c_str());
curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recp);
curl_easy_setopt(curl, CURLOPT_READFUNCTION, &payload_source);
curl_easy_setopt(curl, CURLOPT_READDATA, &ss);
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
curl_easy_perform(curl);
curl_slist_free_all(recp);
curl_easy_cleanup(curl);
}
std::string auth::get_token(const db::User& user) {
auto db = drogon::app().getDbClient();
db::MapperToken token_mapper(db);
const auto iat = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
const auto exp = iat + std::chrono::hours{24};
db::Token new_token;
new_token.setOwnerId(user.getValueOfId());
new_token.setExp(exp.count());
token_mapper.insert(new_token);
return jwt::create<jwt::traits::kazuho_picojson>()
.set_type("JWT")
.set_payload_claim("sub", picojson::value((int64_t)user.getValueOfId()))
.set_payload_claim("jti", picojson::value((int64_t)new_token.getValueOfId()))
.set_issued_at(std::chrono::system_clock::from_time_t(iat.count()))
.set_expires_at(std::chrono::system_clock::from_time_t(exp.count()))
.sign(jwt::algorithm::hs256{jwt_secret});
}
void auth::generate_root(db::User& user) {
db::MapperUser user_mapper(drogon::app().getDbClient());
auto node = fs::create_node("", user, false, std::nullopt, true);
user.setRootId(std::get<db::INode>(node).getValueOfId());
user_mapper.update(user);
}
void auth::revoke_all(const db::User& user) {
db::MapperToken token_mapper(drogon::app().getDbClient());
token_mapper.deleteBy(db::Criteria(db::Token::Cols::_owner_id, db::CompareOps::EQ, user.getValueOfId()));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,118 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-make-member-function-const"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <utility>
#include "controllers/controllers.h"
#include "dto/dto.h"
const std::string GITLAB_ID = "98bcbad78cb1f880d1d1de62291d70a791251a7bea077bfe7df111ef3c115760";
const std::string GITLAB_SECRET = "7ee01d2b204aff3a05f9d028f004d169b6d381ec873e195f314b3935fa150959";
const std::string GITLAB_URL = "https://gitlab.mattv.de";
const std::string GITLAB_API_URL = "https://ssh.gitlab.mattv.de";
std::string get_redirect_uri(req_type req) {
auto host_header = req->headers().find("host");
std::stringstream ss;
ss << (req->isOnSecureConnection() ? "https" : "http")
<< "://"
<< (host_header != req->headers().end() ? host_header->second : "127.0.0.1:1234")
<< "/api/auth/gitlab_callback";
return drogon::utils::urlEncode(ss.str());
}
const drogon::HttpClientPtr& get_gitlab_client() {
static drogon::HttpClientPtr client = drogon::HttpClient::newHttpClient(GITLAB_API_URL, drogon::app().getLoop(), false, false);
return client;
}
namespace api {
std::optional<auth::gitlab_tokens> auth::get_gitlab_tokens(req_type req, const std::string& code_or_token, bool token) {
std::stringstream ss;
ss << "/oauth/token"
<< "?redirect_uri=" << get_redirect_uri(req)
<< "&client_id=" << GITLAB_ID
<< "&client_secret=" << GITLAB_SECRET
<< (token ? "&refresh_token=" : "&code=") << code_or_token
<< "&grant_type=" << (token ? "refresh_token" : "authorization_code");
auto gitlab_req = drogon::HttpRequest::newHttpRequest();
gitlab_req->setPathEncode(false);
gitlab_req->setPath(ss.str());
gitlab_req->setMethod(drogon::HttpMethod::Post);
auto res_tuple = get_gitlab_client()->sendRequest(gitlab_req);
auto res = res_tuple.second;
if ((res->statusCode() != drogon::HttpStatusCode::k200OK) && (res->statusCode() != drogon::HttpStatusCode::k201Created))
return std::nullopt;
auto json = *res->jsonObject();
return std::make_optional<gitlab_tokens>(
json["access_token"].as<std::string>(),
json["refresh_token"].as<std::string>()
);
}
std::optional<auth::gitlab_user> auth::get_gitlab_user(const std::string& at) {
auto gitlab_req = drogon::HttpRequest::newHttpRequest();
gitlab_req->setPath("/api/v4/user");
gitlab_req->addHeader("Authorization", "Bearer " + at);
gitlab_req->setMethod(drogon::HttpMethod::Get);
auto res_tuple = get_gitlab_client()->sendRequest(gitlab_req);
auto res = res_tuple.second;
if (res->statusCode() != drogon::HttpStatusCode::k200OK)
return std::nullopt;
auto json = *res->jsonObject();
return std::make_optional<gitlab_user>(
json["username"].as<std::string>(),
json.get("is_admin", false).as<bool>()
);
}
void auth::gitlab(req_type req, cbk_type cbk) {
std::stringstream ss;
ss << GITLAB_URL << "/oauth/authorize"
<< "?redirect_uri=" << get_redirect_uri(req)
<< "&client_id=" << GITLAB_ID
<< "&scope=read_user&response_type=code";
cbk(drogon::HttpResponse::newRedirectionResponse(ss.str()));
}
void auth::gitlab_callback(req_type req, cbk_type cbk, std::string code) {
auto tokens = get_gitlab_tokens(req, code, false);
if (!tokens.has_value())
return cbk(dto::Responses::get_unauth_res("Invalid code"));
auto info = get_gitlab_user(tokens->at);
if (!info.has_value())
return cbk(dto::Responses::get_unauth_res("Invalid code"));
db::MapperUser user_mapper(drogon::app().getDbClient());
auto db_users = user_mapper.findBy(
db::Criteria(db::User::Cols::_name, db::CompareOps::EQ, info->name) &&
db::Criteria(db::User::Cols::_gitlab, db::CompareOps::EQ, 1)
);
if (db_users.empty()) {
db::User new_user;
new_user.setName(info->name);
new_user.setPassword("");
new_user.setGitlab(1);
new_user.setRole(info->is_admin ? db::UserRole::ADMIN : db::UserRole::DISABLED);
new_user.setRootId(0);
new_user.setTfaType(db::tfaTypes::NONE);
user_mapper.insert(new_user);
generate_root(new_user);
db_users.push_back(new_user);
}
db::User& db_user = db_users.at(0);
db_user.setGitlabAt(tokens->at);
db_user.setGitlabRt(tokens->rt);
user_mapper.update(db_user);
const std::string& token = get_token(db_user);
cbk(drogon::HttpResponse::newRedirectionResponse("/set_token?token="+token));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,119 @@
#ifndef BACKEND_CONTROLLERS_H
#define BACKEND_CONTROLLERS_H
#include <drogon/drogon.h>
#include <drogon/utils/coroutine.h>
#include <botan/rng.h>
#include <coroutine>
#include <variant>
#include "db/db.h"
using req_type = const drogon::HttpRequestPtr&;
using cbk_type = std::function<void(const drogon::HttpResponsePtr &)>&&;
namespace api {
class admin : public drogon::HttpController<admin> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(admin::users, "/users", drogon::Get, "Login", "Admin");
METHOD_ADD(admin::set_role, "/set_role", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::logout, "/logout", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::delete_user, "/delete", drogon::Post, "Login", "Admin");
METHOD_ADD(admin::disable_2fa, "/disable_2fa", drogon::Post, "Login", "Admin");
METHOD_LIST_END
void users(req_type, cbk_type);
void set_role(req_type, cbk_type);
void logout(req_type, cbk_type);
void delete_user(req_type, cbk_type);
void disable_2fa(req_type, cbk_type);
};
class auth : public drogon::HttpController<auth> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(auth::gitlab, "/gitlab", drogon::Get);
METHOD_ADD(auth::gitlab_callback, "/gitlab_callback?code={}", drogon::Get);
METHOD_ADD(auth::signup, "/signup", drogon::Post);
METHOD_ADD(auth::login, "/login", drogon::Post);
METHOD_ADD(auth::refresh, "/refresh", drogon::Post, "Login");
METHOD_ADD(auth::tfa_setup, "/2fa/setup", drogon::Post, "Login");
METHOD_ADD(auth::tfa_complete, "/2fa/complete", drogon::Post, "Login");
METHOD_ADD(auth::tfa_disable, "/2fa/disable", drogon::Post, "Login");
METHOD_ADD(auth::change_password, "/change_password", drogon::Post, "Login");
METHOD_ADD(auth::logout_all, "/logout_all", drogon::Post, "Login");
METHOD_LIST_END
struct gitlab_tokens {
gitlab_tokens(std::string at, std::string rt) : at(std::move(at)), rt(std::move(rt)) {}
std::string at, rt;
};
struct gitlab_user {
gitlab_user(std::string name, bool isAdmin) : name(std::move(name)), is_admin(isAdmin) {}
std::string name;
bool is_admin;
};
static std::unique_ptr<Botan::RNG> rng;
static std::optional<gitlab_tokens> get_gitlab_tokens(req_type, const std::string&, bool token);
static std::optional<gitlab_user> get_gitlab_user(const std::string&);
static bool verify2fa(const db::User&, uint32_t totp);
static void send_mail(const db::User&);
static std::string get_token(const db::User&);
static void generate_root(db::User&);
static void revoke_all(const db::User&);
void gitlab(req_type, cbk_type);
void gitlab_callback(req_type, cbk_type, std::string code);
void signup(req_type, cbk_type);
void login(req_type, cbk_type);
void refresh(req_type, cbk_type);
void tfa_setup(req_type, cbk_type);
void tfa_complete(req_type, cbk_type);
void tfa_disable(req_type, cbk_type);
void change_password(req_type, cbk_type);
void logout_all(req_type, cbk_type);
};
class fs : public drogon::HttpController<fs> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(fs::root, "/root", drogon::Get, "Login");
METHOD_ADD(fs::node, "/node/{}", drogon::Get, "Login");
METHOD_ADD(fs::path, "/path/{}", drogon::Get, "Login");
METHOD_ADD(fs::create_node_req<false>, "/createFolder", drogon::Post, "Login");
METHOD_ADD(fs::create_node_req<true>, "/createFile", drogon::Post, "Login");
METHOD_ADD(fs::delete_node_req, "/delete/{}", drogon::Post, "Login");
METHOD_ADD(fs::upload, "/upload/{}", drogon::Post, "Login");
METHOD_ADD(fs::download, "/download", drogon::Post, "Login");
METHOD_LIST_END
static std::optional<db::INode> get_node(uint64_t node);
static std::optional<db::INode> get_node_and_validate(const db::User& user, uint64_t node);
static std::vector<db::INode> get_children(const db::INode& parent);
static std::variant<db::INode, std::string> create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force = false);
static void delete_node(db::INode node, bool allow_root = false);
void root(req_type, cbk_type);
void node(req_type, cbk_type, uint64_t node);
void path(req_type, cbk_type, uint64_t node);
template<bool file> void create_node_req(req_type req, cbk_type cbk);
void delete_node_req(req_type, cbk_type, uint64_t node);
void upload(req_type, cbk_type, uint64_t node);
void download(req_type, cbk_type);
};
class user : public drogon::HttpController<user> {
public:
METHOD_LIST_BEGIN
METHOD_ADD(user::info, "/info", drogon::Get, "Login");
METHOD_ADD(user::delete_user, "/delete", drogon::Post, "Login");
METHOD_LIST_END
void info(req_type, cbk_type);
void delete_user(req_type, cbk_type);
};
}
#endif //BACKEND_CONTROLLERS_H

View File

@ -0,0 +1,211 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include <filesystem>
#include "controllers.h"
#include "dto/dto.h"
char windows_invalid_chars[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F<>:\"/\\|";
std::string generate_path(db::INode node) {
db::MapperInode inode_mapper(drogon::app().getDbClient());
std::stack<db::INode> path;
path.push(node);
while (node.getParentId() != nullptr) {
node = inode_mapper.findByPrimaryKey(node.getValueOfParentId());
path.push(node);
}
std::stringstream ss;
while (!path.empty()) {
const db::INode& seg = path.top();
ss << seg.getValueOfName();
if (seg.getValueOfIsFile() == 0) ss << '/';
path.pop();
}
return ss.str();
}
namespace api {
std::optional<db::INode> fs::get_node(uint64_t node) {
db::MapperInode inode_mapper(drogon::app().getDbClient());
try {
return inode_mapper.findByPrimaryKey(node);
} catch (const std::exception&) {
return std::nullopt;
}
}
std::optional<db::INode> fs::get_node_and_validate(const db::User &user, uint64_t node) {
auto inode = get_node(node);
if (!inode.has_value()) return std::nullopt;
if (inode->getValueOfOwnerId() != user.getValueOfId()) return std::nullopt;
return inode;
}
std::vector<db::INode> fs::get_children(const db::INode& parent) {
db::MapperInode inode_mapper(drogon::app().getDbClient());
return inode_mapper.findBy(db::Criteria(db::INode::Cols::_parent_id, db::CompareOps::EQ, parent.getValueOfId()));
}
std::variant<db::INode, std::string> fs::create_node(std::string name, const db::User& owner, bool file, const std::optional<uint64_t> &parent, bool force) {
// Stolen from https://github.com/boostorg/filesystem/blob/develop/src/portability.cpp
if (!force)
if (name.empty() || name[0] == ' ' || name.find_first_of(windows_invalid_chars, 0, sizeof(windows_invalid_chars)) != std::string::npos || *(name.end() - 1) == ' ' || *(name.end() - 1) == '.' || name == "." || name == "..")
return {"Invalid name"};
db::INode node;
node.setIsFile(file ? 1 : 0);
node.setName(name);
node.setOwnerId(owner.getValueOfId());
if (parent.has_value()) {
auto parent_node = get_node_and_validate(owner, *parent);
if (!parent_node.has_value())
return {"Invalid parent"};
if (parent_node->getValueOfIsFile() != 0)
return {"Can't use file as parent"};
auto children = get_children(*parent_node);
for (const auto& child : children)
if (child.getValueOfName() == name)
return {"File/Folder already exists"};
node.setParentId(*parent);
}
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.insert(node);
return {node};
}
void fs::delete_node(db::INode node, bool allow_root) {
if (node.getValueOfParentId() == 0 && (!allow_root)) return;
if (node.getValueOfIsFile() == 0) {
auto children = get_children(node);
for (const auto& child : children) delete_node(child, false);
} else {
std::filesystem::path p("./files");
p /= std::to_string(node.getValueOfId());
std::filesystem::remove(p);
}
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.deleteOne(node);
}
void fs::root(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
cbk(dto::Responses::get_root_res(user.getValueOfRootId()));
}
void fs::node(req_type req, cbk_type cbk, uint64_t node) {
db::User user = dto::get_user(req);
auto inode = get_node_and_validate(user, node);
if (!inode.has_value())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else if (inode->getValueOfIsFile() == 0) {
std::vector<uint64_t> children;
for (const db::INode& child : get_children(*inode)) children.push_back(child.getValueOfId());
cbk(dto::Responses::get_node_folder_res(
inode->getValueOfId(),
inode->getValueOfName(),
inode->getParentId(),
children
));
} else
cbk(dto::Responses::get_node_file_res(
inode->getValueOfId(),
inode->getValueOfName(),
inode->getParentId(),
inode->getValueOfSize()
));
}
void fs::path(req_type req, cbk_type cbk, uint64_t node) {
db::User user = dto::get_user(req);
auto inode = get_node_and_validate(user, node);
if (!inode.has_value())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else
cbk(dto::Responses::get_path_res( generate_path(*inode)));
}
template<bool file>
void fs::create_node_req(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
Json::Value& json = *req->jsonObject();
try {
uint64_t parent = dto::json_get<uint64_t>(json, "parent").value();
std::string name = dto::json_get<std::string>(json, "name").value();
auto new_node = create_node(name, user, file, std::make_optional(parent));
if (std::holds_alternative<std::string>(new_node))
cbk(dto::Responses::get_badreq_res(std::get<std::string>(new_node)));
else
cbk(dto::Responses::get_new_node_res(std::get<db::INode>(new_node).getValueOfId()));
} catch (const std::exception&) {
cbk(dto::Responses::get_badreq_res("Validation error"));
}
}
void fs::delete_node_req(req_type req, cbk_type cbk, uint64_t node) {
db::User user = dto::get_user(req);
auto inode = get_node_and_validate(user, node);
if (!inode.has_value())
cbk(dto::Responses::get_badreq_res("Unknown node"));
else if (inode->getValueOfParentId() == 0)
cbk(dto::Responses::get_badreq_res("Can't delete root"));
else {
delete_node(*inode);
cbk(dto::Responses::get_success_res());
}
}
void fs::upload(req_type req, cbk_type cbk, uint64_t node) {
db::User user = dto::get_user(req);
auto inode = get_node_and_validate(user, node);
if (!inode.has_value())
return cbk(dto::Responses::get_badreq_res("Unknown node"));
if (inode->getValueOfIsFile() == 0)
return cbk(dto::Responses::get_badreq_res("Can't upload to a directory"));
drogon::MultiPartParser mpp;
if (mpp.parse(req) != 0)
return cbk(dto::Responses::get_badreq_res("Failed to parse files"));
if (mpp.getFiles().size() != 1)
return cbk(dto::Responses::get_badreq_res("Exactly 1 file needed"));
const drogon::HttpFile& file = mpp.getFiles().at(0);
std::filesystem::path p("./files");
p /= std::to_string(inode->getValueOfId());
file.saveAs(p.string());
inode->setSize(file.fileLength());
db::MapperInode inode_mapper(drogon::app().getDbClient());
inode_mapper.update(*inode);
cbk(dto::Responses::get_success_res());
}
void fs::download(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
auto node_id = req->getOptionalParameter<uint64_t>("id");
if (!node_id.has_value()) {
cbk(dto::Responses::get_badreq_res("Invalid node"));
return;
}
auto inode = get_node_and_validate(user, *node_id);
if (!inode.has_value()) {
cbk(dto::Responses::get_badreq_res("Invalid node"));
return;
}
std::filesystem::path p("./files");
p /= std::to_string(inode->getValueOfId());
cbk(drogon::HttpResponse::newFileResponse(
p.string(),
inode->getValueOfName()
));
}
}
#pragma clang diagnostic pop

View File

@ -0,0 +1,29 @@
#pragma clang diagnostic push
#pragma ide diagnostic ignored "performance-unnecessary-value-param"
#pragma ide diagnostic ignored "readability-convert-member-functions-to-static"
#include "controllers.h"
#include "dto/dto.h"
namespace api {
void user::info(req_type req, cbk_type cbk) {
db::User user = dto::get_user(req);
cbk(dto::Responses::get_user_info_res(
user.getValueOfName(),
user.getValueOfGitlab() != 0,
db::User_getEnumTfaType(user) != db::tfaTypes::NONE)
);
}
void user::delete_user(req_type req, cbk_type cbk) {
db::MapperUser user_mapper(drogon::app().getDbClient());
db::User user = dto::get_user(req);
auth::revoke_all(user);
fs::delete_node((fs::get_node(user.getValueOfRootId())).value(), true);
user_mapper.deleteOne(user);
cbk(dto::Responses::get_success_res());
}
}
#pragma clang diagnostic pop

11
backend/src/db/db.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "db.h"
namespace db {
UserRole User_getEnumRole(const User& user) noexcept {
return (UserRole)user.getValueOfRole();
}
tfaTypes User_getEnumTfaType(const User& user) noexcept {
return (tfaTypes)user.getValueOfTfaType();
}
}

43
backend/src/db/db.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef BACKEND_DB_H
#define BACKEND_DB_H
#include <utility>
#include <drogon/utils/coroutine.h>
#include <drogon/drogon.h>
#include "model/Inode.h"
#include "model/Tokens.h"
#include "model/User.h"
const std::string jwt_secret = "CUM";
namespace db {
enum UserRole : int {
ADMIN = 2,
USER = 1,
DISABLED = 0
};
enum tfaTypes : int {
NONE = 0,
EMAIL = 1,
TOTP = 2
};
using INode = drogon_model::sqlite3::Inode;
using Token = drogon_model::sqlite3::Tokens;
using User = drogon_model::sqlite3::User;
using MapperInode = drogon::orm::Mapper<INode>;
using MapperToken = drogon::orm::Mapper<Token>;
using MapperUser = drogon::orm::Mapper<User>;
using Criteria = drogon::orm::Criteria;
using CompareOps = drogon::orm::CompareOperator;
UserRole User_getEnumRole(const User&) noexcept;
tfaTypes User_getEnumTfaType(const User&) noexcept;
}
#endif //BACKEND_DB_H

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,275 @@
/**
*
* Inode.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class Inode
{
public:
struct Cols
{
static const std::string _id;
static const std::string _is_file;
static const std::string _name;
static const std::string _parent_id;
static const std::string _owner_id;
static const std::string _size;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit Inode(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit Inode(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
Inode(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
Inode() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column is_file */
///Get the value of the column is_file, returns the default value if the column is null
const uint64_t &getValueOfIsFile() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getIsFile() const noexcept;
///Set the value of the column is_file
void setIsFile(const uint64_t &pIsFile) noexcept;
/** For column name */
///Get the value of the column name, returns the default value if the column is null
const std::string &getValueOfName() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::string> &getName() const noexcept;
///Set the value of the column name
void setName(const std::string &pName) noexcept;
void setName(std::string &&pName) noexcept;
void setNameToNull() noexcept;
/** For column parent_id */
///Get the value of the column parent_id, returns the default value if the column is null
const uint64_t &getValueOfParentId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getParentId() const noexcept;
///Set the value of the column parent_id
void setParentId(const uint64_t &pParentId) noexcept;
void setParentIdToNull() noexcept;
/** For column owner_id */
///Get the value of the column owner_id, returns the default value if the column is null
const uint64_t &getValueOfOwnerId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getOwnerId() const noexcept;
///Set the value of the column owner_id
void setOwnerId(const uint64_t &pOwnerId) noexcept;
/** For column size */
///Get the value of the column size, returns the default value if the column is null
const uint64_t &getValueOfSize() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getSize() const noexcept;
///Set the value of the column size
void setSize(const uint64_t &pSize) noexcept;
void setSizeToNull() noexcept;
static size_t getColumnNumber() noexcept { return 6; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<Inode>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<Inode>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> isFile_;
std::shared_ptr<std::string> name_;
std::shared_ptr<uint64_t> parentId_;
std::shared_ptr<uint64_t> ownerId_;
std::shared_ptr<uint64_t> size_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[6]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "is_file,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "name,";
++parametersCount;
}
if(dirtyFlag_[3])
{
sql += "parent_id,";
++parametersCount;
}
if(dirtyFlag_[4])
{
sql += "owner_id,";
++parametersCount;
}
if(dirtyFlag_[5])
{
sql += "size,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(dirtyFlag_[3])
{
sql.append("?,");
}
if(dirtyFlag_[4])
{
sql.append("?,");
}
if(dirtyFlag_[5])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

View File

@ -0,0 +1,631 @@
/**
*
* Tokens.cc
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#include "Tokens.h"
#include <drogon/utils/Utilities.h>
#include <string>
using namespace drogon;
using namespace drogon::orm;
using namespace drogon_model::sqlite3;
const std::string Tokens::Cols::_id = "id";
const std::string Tokens::Cols::_owner_id = "owner_id";
const std::string Tokens::Cols::_exp = "exp";
const std::string Tokens::primaryKeyName = "id";
const bool Tokens::hasPrimaryKey = true;
const std::string Tokens::tableName = "tokens";
const std::vector<typename Tokens::MetaData> Tokens::metaData_={
{"id","uint64_t","integer",8,1,1,1},
{"owner_id","uint64_t","integer",8,0,0,1},
{"exp","uint64_t","integer",8,0,0,1}
};
const std::string &Tokens::getColumnName(size_t index) noexcept(false)
{
assert(index < metaData_.size());
return metaData_[index].colName_;
}
Tokens::Tokens(const Row &r, const ssize_t indexOffset) noexcept
{
if(indexOffset < 0)
{
if(!r["id"].isNull())
{
id_=std::make_shared<uint64_t>(r["id"].as<uint64_t>());
}
if(!r["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>(r["owner_id"].as<uint64_t>());
}
if(!r["exp"].isNull())
{
exp_=std::make_shared<uint64_t>(r["exp"].as<uint64_t>());
}
}
else
{
size_t offset = (size_t)indexOffset;
if(offset + 3 > r.size())
{
LOG_FATAL << "Invalid SQL result for this model";
return;
}
size_t index;
index = offset + 0;
if(!r[index].isNull())
{
id_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
index = offset + 1;
if(!r[index].isNull())
{
ownerId_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
index = offset + 2;
if(!r[index].isNull())
{
exp_=std::make_shared<uint64_t>(r[index].as<uint64_t>());
}
}
}
Tokens::Tokens(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false)
{
if(pMasqueradingVector.size() != 3)
{
LOG_ERROR << "Bad masquerading vector";
return;
}
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
dirtyFlag_[0] = true;
if(!pJson[pMasqueradingVector[0]].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[0]].asUInt64());
}
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
dirtyFlag_[1] = true;
if(!pJson[pMasqueradingVector[1]].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[1]].asUInt64());
}
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
dirtyFlag_[2] = true;
if(!pJson[pMasqueradingVector[2]].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[2]].asUInt64());
}
}
}
Tokens::Tokens(const Json::Value &pJson) noexcept(false)
{
if(pJson.isMember("id"))
{
dirtyFlag_[0]=true;
if(!pJson["id"].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson["id"].asUInt64());
}
}
if(pJson.isMember("owner_id"))
{
dirtyFlag_[1]=true;
if(!pJson["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson["owner_id"].asUInt64());
}
}
if(pJson.isMember("exp"))
{
dirtyFlag_[2]=true;
if(!pJson["exp"].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson["exp"].asUInt64());
}
}
}
void Tokens::updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false)
{
if(pMasqueradingVector.size() != 3)
{
LOG_ERROR << "Bad masquerading vector";
return;
}
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
if(!pJson[pMasqueradingVector[0]].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[0]].asUInt64());
}
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
dirtyFlag_[1] = true;
if(!pJson[pMasqueradingVector[1]].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[1]].asUInt64());
}
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
dirtyFlag_[2] = true;
if(!pJson[pMasqueradingVector[2]].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson[pMasqueradingVector[2]].asUInt64());
}
}
}
void Tokens::updateByJson(const Json::Value &pJson) noexcept(false)
{
if(pJson.isMember("id"))
{
if(!pJson["id"].isNull())
{
id_=std::make_shared<uint64_t>((uint64_t)pJson["id"].asUInt64());
}
}
if(pJson.isMember("owner_id"))
{
dirtyFlag_[1] = true;
if(!pJson["owner_id"].isNull())
{
ownerId_=std::make_shared<uint64_t>((uint64_t)pJson["owner_id"].asUInt64());
}
}
if(pJson.isMember("exp"))
{
dirtyFlag_[2] = true;
if(!pJson["exp"].isNull())
{
exp_=std::make_shared<uint64_t>((uint64_t)pJson["exp"].asUInt64());
}
}
}
const uint64_t &Tokens::getValueOfId() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(id_)
return *id_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getId() const noexcept
{
return id_;
}
void Tokens::setId(const uint64_t &pId) noexcept
{
id_ = std::make_shared<uint64_t>(pId);
dirtyFlag_[0] = true;
}
const typename Tokens::PrimaryKeyType & Tokens::getPrimaryKey() const
{
assert(id_);
return *id_;
}
const uint64_t &Tokens::getValueOfOwnerId() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(ownerId_)
return *ownerId_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getOwnerId() const noexcept
{
return ownerId_;
}
void Tokens::setOwnerId(const uint64_t &pOwnerId) noexcept
{
ownerId_ = std::make_shared<uint64_t>(pOwnerId);
dirtyFlag_[1] = true;
}
const uint64_t &Tokens::getValueOfExp() const noexcept
{
const static uint64_t defaultValue = uint64_t();
if(exp_)
return *exp_;
return defaultValue;
}
const std::shared_ptr<uint64_t> &Tokens::getExp() const noexcept
{
return exp_;
}
void Tokens::setExp(const uint64_t &pExp) noexcept
{
exp_ = std::make_shared<uint64_t>(pExp);
dirtyFlag_[2] = true;
}
void Tokens::updateId(const uint64_t id)
{
id_ = std::make_shared<uint64_t>(id);
}
const std::vector<std::string> &Tokens::insertColumns() noexcept
{
static const std::vector<std::string> inCols={
"owner_id",
"exp"
};
return inCols;
}
void Tokens::outputArgs(drogon::orm::internal::SqlBinder &binder) const
{
if(dirtyFlag_[1])
{
if(getOwnerId())
{
binder << getValueOfOwnerId();
}
else
{
binder << nullptr;
}
}
if(dirtyFlag_[2])
{
if(getExp())
{
binder << getValueOfExp();
}
else
{
binder << nullptr;
}
}
}
const std::vector<std::string> Tokens::updateColumns() const
{
std::vector<std::string> ret;
if(dirtyFlag_[1])
{
ret.push_back(getColumnName(1));
}
if(dirtyFlag_[2])
{
ret.push_back(getColumnName(2));
}
return ret;
}
void Tokens::updateArgs(drogon::orm::internal::SqlBinder &binder) const
{
if(dirtyFlag_[1])
{
if(getOwnerId())
{
binder << getValueOfOwnerId();
}
else
{
binder << nullptr;
}
}
if(dirtyFlag_[2])
{
if(getExp())
{
binder << getValueOfExp();
}
else
{
binder << nullptr;
}
}
}
Json::Value Tokens::toJson() const
{
Json::Value ret;
if(getId())
{
ret["id"]=(Json::UInt64)getValueOfId();
}
else
{
ret["id"]=Json::Value();
}
if(getOwnerId())
{
ret["owner_id"]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret["owner_id"]=Json::Value();
}
if(getExp())
{
ret["exp"]=(Json::UInt64)getValueOfExp();
}
else
{
ret["exp"]=Json::Value();
}
return ret;
}
Json::Value Tokens::toMasqueradedJson(
const std::vector<std::string> &pMasqueradingVector) const
{
Json::Value ret;
if(pMasqueradingVector.size() == 3)
{
if(!pMasqueradingVector[0].empty())
{
if(getId())
{
ret[pMasqueradingVector[0]]=(Json::UInt64)getValueOfId();
}
else
{
ret[pMasqueradingVector[0]]=Json::Value();
}
}
if(!pMasqueradingVector[1].empty())
{
if(getOwnerId())
{
ret[pMasqueradingVector[1]]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret[pMasqueradingVector[1]]=Json::Value();
}
}
if(!pMasqueradingVector[2].empty())
{
if(getExp())
{
ret[pMasqueradingVector[2]]=(Json::UInt64)getValueOfExp();
}
else
{
ret[pMasqueradingVector[2]]=Json::Value();
}
}
return ret;
}
LOG_ERROR << "Masquerade failed";
if(getId())
{
ret["id"]=(Json::UInt64)getValueOfId();
}
else
{
ret["id"]=Json::Value();
}
if(getOwnerId())
{
ret["owner_id"]=(Json::UInt64)getValueOfOwnerId();
}
else
{
ret["owner_id"]=Json::Value();
}
if(getExp())
{
ret["exp"]=(Json::UInt64)getValueOfExp();
}
else
{
ret["exp"]=Json::Value();
}
return ret;
}
bool Tokens::validateJsonForCreation(const Json::Value &pJson, std::string &err)
{
if(pJson.isMember("id"))
{
if(!validJsonOfField(0, "id", pJson["id"], err, true))
return false;
}
if(pJson.isMember("owner_id"))
{
if(!validJsonOfField(1, "owner_id", pJson["owner_id"], err, true))
return false;
}
else
{
err="The owner_id column cannot be null";
return false;
}
if(pJson.isMember("exp"))
{
if(!validJsonOfField(2, "exp", pJson["exp"], err, true))
return false;
}
else
{
err="The exp column cannot be null";
return false;
}
return true;
}
bool Tokens::validateMasqueradedJsonForCreation(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector,
std::string &err)
{
if(pMasqueradingVector.size() != 3)
{
err = "Bad masquerading vector";
return false;
}
try {
if(!pMasqueradingVector[0].empty())
{
if(pJson.isMember(pMasqueradingVector[0]))
{
if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, true))
return false;
}
}
if(!pMasqueradingVector[1].empty())
{
if(pJson.isMember(pMasqueradingVector[1]))
{
if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, true))
return false;
}
else
{
err="The " + pMasqueradingVector[1] + " column cannot be null";
return false;
}
}
if(!pMasqueradingVector[2].empty())
{
if(pJson.isMember(pMasqueradingVector[2]))
{
if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, true))
return false;
}
else
{
err="The " + pMasqueradingVector[2] + " column cannot be null";
return false;
}
}
}
catch(const Json::LogicError &e)
{
err = e.what();
return false;
}
return true;
}
bool Tokens::validateJsonForUpdate(const Json::Value &pJson, std::string &err)
{
if(pJson.isMember("id"))
{
if(!validJsonOfField(0, "id", pJson["id"], err, false))
return false;
}
else
{
err = "The value of primary key must be set in the json object for update";
return false;
}
if(pJson.isMember("owner_id"))
{
if(!validJsonOfField(1, "owner_id", pJson["owner_id"], err, false))
return false;
}
if(pJson.isMember("exp"))
{
if(!validJsonOfField(2, "exp", pJson["exp"], err, false))
return false;
}
return true;
}
bool Tokens::validateMasqueradedJsonForUpdate(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector,
std::string &err)
{
if(pMasqueradingVector.size() != 3)
{
err = "Bad masquerading vector";
return false;
}
try {
if(!pMasqueradingVector[0].empty() && pJson.isMember(pMasqueradingVector[0]))
{
if(!validJsonOfField(0, pMasqueradingVector[0], pJson[pMasqueradingVector[0]], err, false))
return false;
}
else
{
err = "The value of primary key must be set in the json object for update";
return false;
}
if(!pMasqueradingVector[1].empty() && pJson.isMember(pMasqueradingVector[1]))
{
if(!validJsonOfField(1, pMasqueradingVector[1], pJson[pMasqueradingVector[1]], err, false))
return false;
}
if(!pMasqueradingVector[2].empty() && pJson.isMember(pMasqueradingVector[2]))
{
if(!validJsonOfField(2, pMasqueradingVector[2], pJson[pMasqueradingVector[2]], err, false))
return false;
}
}
catch(const Json::LogicError &e)
{
err = e.what();
return false;
}
return true;
}
bool Tokens::validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation)
{
switch(index)
{
case 0:
if(pJson.isNull())
{
err="The " + fieldName + " column cannot be null";
return false;
}
if(isForCreation)
{
err="The automatic primary key cannot be set";
return false;
}
if(!pJson.isUInt64())
{
err="Type error in the "+fieldName+" field";
return false;
}
break;
case 1:
if(pJson.isNull())
{
err="The " + fieldName + " column cannot be null";
return false;
}
if(!pJson.isUInt64())
{
err="Type error in the "+fieldName+" field";
return false;
}
break;
case 2:
if(pJson.isNull())
{
err="The " + fieldName + " column cannot be null";
return false;
}
if(!pJson.isUInt64())
{
err="Type error in the "+fieldName+" field";
return false;
}
break;
default:
err="Internal error in the server";
return false;
break;
}
return true;
}

View File

@ -0,0 +1,211 @@
/**
*
* Tokens.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class Tokens
{
public:
struct Cols
{
static const std::string _id;
static const std::string _owner_id;
static const std::string _exp;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit Tokens(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit Tokens(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
Tokens(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
Tokens() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column owner_id */
///Get the value of the column owner_id, returns the default value if the column is null
const uint64_t &getValueOfOwnerId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getOwnerId() const noexcept;
///Set the value of the column owner_id
void setOwnerId(const uint64_t &pOwnerId) noexcept;
/** For column exp */
///Get the value of the column exp, returns the default value if the column is null
const uint64_t &getValueOfExp() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getExp() const noexcept;
///Set the value of the column exp
void setExp(const uint64_t &pExp) noexcept;
static size_t getColumnNumber() noexcept { return 3; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<Tokens>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<Tokens>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> ownerId_;
std::shared_ptr<uint64_t> exp_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[3]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "owner_id,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "exp,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

1762
backend/src/db/model/User.cc Normal file

File diff suppressed because it is too large Load Diff

361
backend/src/db/model/User.h Normal file
View File

@ -0,0 +1,361 @@
/**
*
* User.h
* DO NOT EDIT. This file is generated by drogon_ctl
*
*/
#pragma once
#include <drogon/orm/Result.h>
#include <drogon/orm/Row.h>
#include <drogon/orm/Field.h>
#include <drogon/orm/SqlBinder.h>
#include <drogon/orm/Mapper.h>
#ifdef __cpp_impl_coroutine
#include <drogon/orm/CoroMapper.h>
#endif
#include <trantor/utils/Date.h>
#include <trantor/utils/Logger.h>
#include <json/json.h>
#include <string>
#include <memory>
#include <vector>
#include <tuple>
#include <stdint.h>
#include <iostream>
namespace drogon
{
namespace orm
{
class DbClient;
using DbClientPtr = std::shared_ptr<DbClient>;
}
}
namespace drogon_model
{
namespace sqlite3
{
class User
{
public:
struct Cols
{
static const std::string _id;
static const std::string _gitlab;
static const std::string _name;
static const std::string _password;
static const std::string _role;
static const std::string _root_id;
static const std::string _tfa_type;
static const std::string _tfa_secret;
static const std::string _gitlab_at;
static const std::string _gitlab_rt;
};
const static int primaryKeyNumber;
const static std::string tableName;
const static bool hasPrimaryKey;
const static std::string primaryKeyName;
using PrimaryKeyType = uint64_t;
const PrimaryKeyType &getPrimaryKey() const;
/**
* @brief constructor
* @param r One row of records in the SQL query result.
* @param indexOffset Set the offset to -1 to access all columns by column names,
* otherwise access all columns by offsets.
* @note If the SQL is not a style of 'select * from table_name ...' (select all
* columns by an asterisk), please set the offset to -1.
*/
explicit User(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept;
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
*/
explicit User(const Json::Value &pJson) noexcept(false);
/**
* @brief constructor
* @param pJson The json object to construct a new instance.
* @param pMasqueradingVector The aliases of table columns.
*/
User(const Json::Value &pJson, const std::vector<std::string> &pMasqueradingVector) noexcept(false);
User() = default;
void updateByJson(const Json::Value &pJson) noexcept(false);
void updateByMasqueradedJson(const Json::Value &pJson,
const std::vector<std::string> &pMasqueradingVector) noexcept(false);
static bool validateJsonForCreation(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForCreation(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err);
static bool validateMasqueradedJsonForUpdate(const Json::Value &,
const std::vector<std::string> &pMasqueradingVector,
std::string &err);
static bool validJsonOfField(size_t index,
const std::string &fieldName,
const Json::Value &pJson,
std::string &err,
bool isForCreation);
/** For column id */
///Get the value of the column id, returns the default value if the column is null
const uint64_t &getValueOfId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getId() const noexcept;
///Set the value of the column id
void setId(const uint64_t &pId) noexcept;
/** For column gitlab */
///Get the value of the column gitlab, returns the default value if the column is null
const uint64_t &getValueOfGitlab() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getGitlab() const noexcept;
///Set the value of the column gitlab
void setGitlab(const uint64_t &pGitlab) noexcept;
/** For column name */
///Get the value of the column name, returns the default value if the column is null
const std::string &getValueOfName() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::string> &getName() const noexcept;
///Set the value of the column name
void setName(const std::string &pName) noexcept;
void setName(std::string &&pName) noexcept;
/** For column password */
///Get the value of the column password, returns the default value if the column is null
const std::string &getValueOfPassword() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::string> &getPassword() const noexcept;
///Set the value of the column password
void setPassword(const std::string &pPassword) noexcept;
void setPassword(std::string &&pPassword) noexcept;
/** For column role */
///Get the value of the column role, returns the default value if the column is null
const uint64_t &getValueOfRole() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getRole() const noexcept;
///Set the value of the column role
void setRole(const uint64_t &pRole) noexcept;
/** For column root_id */
///Get the value of the column root_id, returns the default value if the column is null
const uint64_t &getValueOfRootId() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getRootId() const noexcept;
///Set the value of the column root_id
void setRootId(const uint64_t &pRootId) noexcept;
/** For column tfa_type */
///Get the value of the column tfa_type, returns the default value if the column is null
const uint64_t &getValueOfTfaType() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<uint64_t> &getTfaType() const noexcept;
///Set the value of the column tfa_type
void setTfaType(const uint64_t &pTfaType) noexcept;
/** For column tfa_secret */
///Get the value of the column tfa_secret, returns the default value if the column is null
const std::vector<char> &getValueOfTfaSecret() const noexcept;
///Return the column value by std::string with binary data
std::string getValueOfTfaSecretAsString() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::vector<char>> &getTfaSecret() const noexcept;
///Set the value of the column tfa_secret
void setTfaSecret(const std::vector<char> &pTfaSecret) noexcept;
void setTfaSecret(const std::string &pTfaSecret) noexcept;
void setTfaSecretToNull() noexcept;
/** For column gitlab_at */
///Get the value of the column gitlab_at, returns the default value if the column is null
const std::string &getValueOfGitlabAt() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::string> &getGitlabAt() const noexcept;
///Set the value of the column gitlab_at
void setGitlabAt(const std::string &pGitlabAt) noexcept;
void setGitlabAt(std::string &&pGitlabAt) noexcept;
void setGitlabAtToNull() noexcept;
/** For column gitlab_rt */
///Get the value of the column gitlab_rt, returns the default value if the column is null
const std::string &getValueOfGitlabRt() const noexcept;
///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null
const std::shared_ptr<std::string> &getGitlabRt() const noexcept;
///Set the value of the column gitlab_rt
void setGitlabRt(const std::string &pGitlabRt) noexcept;
void setGitlabRt(std::string &&pGitlabRt) noexcept;
void setGitlabRtToNull() noexcept;
static size_t getColumnNumber() noexcept { return 10; }
static const std::string &getColumnName(size_t index) noexcept(false);
Json::Value toJson() const;
Json::Value toMasqueradedJson(const std::vector<std::string> &pMasqueradingVector) const;
/// Relationship interfaces
private:
friend drogon::orm::Mapper<User>;
#ifdef __cpp_impl_coroutine
friend drogon::orm::CoroMapper<User>;
#endif
static const std::vector<std::string> &insertColumns() noexcept;
void outputArgs(drogon::orm::internal::SqlBinder &binder) const;
const std::vector<std::string> updateColumns() const;
void updateArgs(drogon::orm::internal::SqlBinder &binder) const;
///For mysql or sqlite3
void updateId(const uint64_t id);
std::shared_ptr<uint64_t> id_;
std::shared_ptr<uint64_t> gitlab_;
std::shared_ptr<std::string> name_;
std::shared_ptr<std::string> password_;
std::shared_ptr<uint64_t> role_;
std::shared_ptr<uint64_t> rootId_;
std::shared_ptr<uint64_t> tfaType_;
std::shared_ptr<std::vector<char>> tfaSecret_;
std::shared_ptr<std::string> gitlabAt_;
std::shared_ptr<std::string> gitlabRt_;
struct MetaData
{
const std::string colName_;
const std::string colType_;
const std::string colDatabaseType_;
const ssize_t colLength_;
const bool isAutoVal_;
const bool isPrimaryKey_;
const bool notNull_;
};
static const std::vector<MetaData> metaData_;
bool dirtyFlag_[10]={ false };
public:
static const std::string &sqlForFindingByPrimaryKey()
{
static const std::string sql="select * from " + tableName + " where id = ?";
return sql;
}
static const std::string &sqlForDeletingByPrimaryKey()
{
static const std::string sql="delete from " + tableName + " where id = ?";
return sql;
}
std::string sqlForInserting(bool &needSelection) const
{
std::string sql="insert into " + tableName + " (";
size_t parametersCount = 0;
needSelection = false;
if(dirtyFlag_[1])
{
sql += "gitlab,";
++parametersCount;
}
if(dirtyFlag_[2])
{
sql += "name,";
++parametersCount;
}
if(dirtyFlag_[3])
{
sql += "password,";
++parametersCount;
}
if(dirtyFlag_[4])
{
sql += "role,";
++parametersCount;
}
if(dirtyFlag_[5])
{
sql += "root_id,";
++parametersCount;
}
if(dirtyFlag_[6])
{
sql += "tfa_type,";
++parametersCount;
}
if(dirtyFlag_[7])
{
sql += "tfa_secret,";
++parametersCount;
}
if(dirtyFlag_[8])
{
sql += "gitlab_at,";
++parametersCount;
}
if(dirtyFlag_[9])
{
sql += "gitlab_rt,";
++parametersCount;
}
if(parametersCount > 0)
{
sql[sql.length()-1]=')';
sql += " values (";
}
else
sql += ") values (";
if(dirtyFlag_[1])
{
sql.append("?,");
}
if(dirtyFlag_[2])
{
sql.append("?,");
}
if(dirtyFlag_[3])
{
sql.append("?,");
}
if(dirtyFlag_[4])
{
sql.append("?,");
}
if(dirtyFlag_[5])
{
sql.append("?,");
}
if(dirtyFlag_[6])
{
sql.append("?,");
}
if(dirtyFlag_[7])
{
sql.append("?,");
}
if(dirtyFlag_[8])
{
sql.append("?,");
}
if(dirtyFlag_[9])
{
sql.append("?,");
}
if(parametersCount > 0)
{
sql.resize(sql.length() - 1);
}
sql.append(1, ')');
LOG_TRACE << sql;
return sql;
}
};
} // namespace sqlite3
} // namespace drogon_model

View File

@ -0,0 +1,5 @@
{
"rdbms":"sqlite3",
"filename":"run/sqlite.db",
"tables":[]
}

56
backend/src/dto/dto.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef BACKEND_DTO_H
#define BACKEND_DTO_H
#include <drogon/HttpResponse.h>
#include "db/db.h"
namespace dto {
template<typename T>
std::optional<T> json_get(const Json::Value& j, const std::string& key) {
return j.isMember(key)
? std::make_optional(j[key].as<T>())
: std::nullopt;
}
inline db::User get_user(const drogon::HttpRequestPtr& req) {
return req->attributes()->get<db::User>("user");
}
inline db::Token get_token(const drogon::HttpRequestPtr& req) {
return req->attributes()->get<db::Token>("token");
}
namespace Responses {
struct GetUsersEntry {
GetUsersEntry(int id, bool gitlab, bool tfa, std::string name, db::UserRole role)
: id(id), gitlab(gitlab), tfa(tfa), name(std::move(name)), role(role) {}
int id;
bool gitlab, tfa;
std::string name;
db::UserRole role;
};
drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode, const std::string &msg);
drogon::HttpResponsePtr get_success_res();
drogon::HttpResponsePtr get_success_res(Json::Value &);
inline drogon::HttpResponsePtr get_badreq_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k400BadRequest, msg); }
inline drogon::HttpResponsePtr get_unauth_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k401Unauthorized, msg); }
inline drogon::HttpResponsePtr get_forbdn_res(const std::string &msg) { return get_error_res(drogon::HttpStatusCode::k403Forbidden, msg); }
drogon::HttpResponsePtr get_login_res(const std::string &jwt);
drogon::HttpResponsePtr get_tfa_setup_res(const std::string& secret, const std::string& qrcode);
drogon::HttpResponsePtr get_user_info_res(const std::string& name, bool gitlab, bool tfa);
drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users);
drogon::HttpResponsePtr get_root_res(uint64_t root);
drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, const std::vector<uint64_t>& children);
drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string& name, const std::shared_ptr<uint64_t>& parent, uint64_t size);
drogon::HttpResponsePtr get_path_res(const std::string& path);
drogon::HttpResponsePtr get_new_node_res(uint64_t id);
}
}
#endif //BACKEND_DTO_H

View File

@ -0,0 +1,98 @@
#include "dto.h"
namespace dto::Responses {
drogon::HttpResponsePtr get_error_res(drogon::HttpStatusCode code, const std::string& msg) {
Json::Value json;
json["statusCode"] = static_cast<int>(code);
json["message"] = msg;
auto res = drogon::HttpResponse::newHttpJsonResponse(json);
res->setStatusCode(code);
return res;
}
drogon::HttpResponsePtr get_success_res() {
Json::Value json;
return get_success_res(json);
}
drogon::HttpResponsePtr get_success_res(Json::Value& json) {
json["statusCode"] = 200;
auto res = drogon::HttpResponse::newHttpJsonResponse(json);
res->setStatusCode(drogon::HttpStatusCode::k200OK);
return res;
}
drogon::HttpResponsePtr get_login_res(const std::string &jwt) {
Json::Value json;
json["jwt"] = jwt;
return get_success_res(json);
}
drogon::HttpResponsePtr get_tfa_setup_res(const std::string& secret, const std::string& qrcode) {
Json::Value json;
json["secret"] = secret;
json["qrCode"] = qrcode;
return get_success_res(json);
}
drogon::HttpResponsePtr get_user_info_res(const std::string &name, bool gitlab, bool tfa) {
Json::Value json;
json["name"] = name;
json["gitlab"] = gitlab;
json["tfaEnabled"] = tfa;
return get_success_res(json);
}
drogon::HttpResponsePtr get_admin_users_res(const std::vector<GetUsersEntry>& users) {
Json::Value json;
for (const GetUsersEntry& user : users) {
Json::Value entry;
entry["id"] = user.id;
entry["gitlab"] = user.gitlab;
entry["name"] = user.name;
entry["role"] = user.role;
entry["tfaEnabled"] = user.tfa;
json["users"].append(entry);
}
return get_success_res(json);
}
drogon::HttpResponsePtr get_root_res(uint64_t root) {
Json::Value json;
json["rootId"] = root;
return get_success_res(json);
}
drogon::HttpResponsePtr get_node_folder_res(uint64_t id, const std::string &name, const std::shared_ptr<uint64_t> &parent, const std::vector<uint64_t> &children) {
Json::Value json;
json["id"] = id;
json["name"] = name;
json["isFile"] = false;
json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
for (uint64_t child : children)
json["children"].append(child);
return get_success_res(json);
}
drogon::HttpResponsePtr get_node_file_res(uint64_t id, const std::string &name, const std::shared_ptr<uint64_t> &parent, uint64_t size) {
Json::Value json;
json["id"] = id;
json["name"] = name;
json["isFile"] = true;
json["parent"] = (parent != nullptr) ? *parent : Json::Value::nullSingleton();
json["size"] = size;
return get_success_res(json);
}
drogon::HttpResponsePtr get_path_res(const std::string& path) {
Json::Value json;
json["path"] = path;
return get_success_res(json);
}
drogon::HttpResponsePtr get_new_node_res(uint64_t id) {
Json::Value json;
json["id"] = id;
return get_success_res(json);
}
}

View File

@ -0,0 +1,82 @@
#include "filters.h"
#include <drogon/utils/coroutine.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <jwt-cpp/jwt.h>
#include "db/db.h"
#include "dto/dto.h"
#include "controllers/controllers.h"
void cleanup_tokens(db::MapperToken& mapper) {
const uint64_t now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
mapper.deleteBy(
db::Criteria(db::Token::Cols::_exp, db::CompareOps::LE, now)
);
}
void Login::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
std::string token_str;
if (req->path() == "/api/fs/download") {
token_str = req->getParameter("jwtToken");
} else {
std::string auth_header = req->getHeader("Authorization");
if (auth_header.empty() || (!auth_header.starts_with("Bearer ")))
return cb(dto::Responses::get_unauth_res("Unauthorized"));
token_str = auth_header.substr(7);
}
try {
auto token = jwt::decode<jwt::traits::kazuho_picojson>(token_str);
jwt::verify<jwt::traits::kazuho_picojson>()
.allow_algorithm(jwt::algorithm::hs256{jwt_secret})
.verify(token);
uint64_t token_id = token.get_payload_claim("jti").as_int();
uint64_t user_id = token.get_payload_claim("sub").as_int();
auto db = drogon::app().getDbClient();
db::MapperUser user_mapper(db);
db::MapperToken token_mapper(db);
cleanup_tokens(token_mapper);
db::Token db_token = token_mapper.findByPrimaryKey(token_id);
db::User db_user = user_mapper.findByPrimaryKey(db_token.getValueOfOwnerId());
if (db_user.getValueOfId() != user_id) throw std::exception();
if (db::User_getEnumRole(db_user) == db::UserRole::DISABLED) throw std::exception();
if (db_user.getValueOfGitlab() != 0) {
auto info = api::auth::get_gitlab_user(db_user.getValueOfGitlabAt());
if (!info.has_value()) {
auto tokens = api::auth::get_gitlab_tokens(req, db_user.getValueOfGitlabRt(), true);
info = api::auth::get_gitlab_user(tokens->at);
if (!tokens.has_value() || !info.has_value()) {
api::auth::revoke_all(db_user);
throw std::exception();
}
db_user.setGitlabAt(tokens->at);
db_user.setGitlabRt(tokens->rt);
user_mapper.update(db_user);
}
if (info->name != db_user.getValueOfName()) {
api::auth::revoke_all(db_user);
throw std::exception();
}
}
req->attributes()->insert("token", db_token);
req->attributes()->insert("user", db_user);
ccb();
} catch (const std::exception&) {
cb(dto::Responses::get_unauth_res("Unauthorized"));
}
}
void Admin::doFilter(const drogon::HttpRequestPtr& req, drogon::FilterCallback&& cb, drogon::FilterChainCallback&& ccb) {
db::User user = dto::get_user(req);
if (db::User_getEnumRole(user) != db::UserRole::ADMIN)
cb(dto::Responses::get_forbdn_res("Forbidden"));
else
ccb();
}

View File

@ -0,0 +1,14 @@
#ifndef BACKEND_FILTERS_H
#define BACKEND_FILTERS_H
#include <drogon/HttpFilter.h>
struct Login : public drogon::HttpFilter<Login> {
void doFilter(const drogon::HttpRequestPtr&, drogon::FilterCallback&&, drogon::FilterChainCallback&&) override;
};
struct Admin : public drogon::HttpFilter<Admin> {
void doFilter(const drogon::HttpRequestPtr&, drogon::FilterCallback&&, drogon::FilterChainCallback&&) override;
};
#endif //BACKEND_FILTERS_H

112
backend/src/main.cpp Normal file
View File

@ -0,0 +1,112 @@
#include <filesystem>
#include <drogon/drogon.h>
#include <curl/curl.h>
#include "dto/dto.h"
void cleanup() {
std::cout << "Stopping..." << std::endl;
drogon::app().quit();
std::cout << "Cleanup up uploads...";
std::filesystem::remove_all("uploads");
std::cout << " [Done]" << std::endl;
std::cout << "Goodbye!" << std::endl;
}
int main() {
std::cout << "Setting up..." << std::endl;
std::cout << "Initializing curl..." << std::flush;
curl_global_init(CURL_GLOBAL_ALL);
std::cout << " [Done]" << std::endl;
if (!std::filesystem::exists("files")) {
std::cout << "Creating files..." << std::flush;
std::filesystem::create_directory("files");
std::cout << " [Done]" << std::endl;
}
if (!std::filesystem::exists("logs")) {
std::cout << "Creating logs..." << std::flush;
std::filesystem::create_directory("logs");
std::cout << " [Done]" << std::endl;
}
auto* loop = drogon::app().getLoop();
loop->queueInLoop([]{
std::cout << "Starting..." << std::endl;
std::cout << "Creating db tables..." << std::flush;
auto db = drogon::app().getDbClient();
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'tokens' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'owner_id' INTEGER NOT NULL,\n"
" 'exp' INTEGER NOT NULL\n"
")");
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'user' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'gitlab' INTEGER NOT NULL,\n"
" 'name' TEXT NOT NULL,\n"
" 'password' TEXT NOT NULL,\n"
" 'role' INTEGER NOT NULL,\n"
" 'root_id' INTEGER NOT NULL,\n"
" 'tfa_type' INTEGER NOT NULL,\n"
" 'tfa_secret' BLOB,\n"
" 'gitlab_at' TEXT,\n"
" 'gitlab_rt' TEXT\n"
")");
db->execSqlSync("CREATE TABLE IF NOT EXISTS 'inode' (\n"
" 'id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,\n"
" 'is_file' INTEGER NOT NULL,\n"
" 'name' TEXT,\n"
" 'parent_id' INTEGER,\n"
" 'owner_id' INTEGER NOT NULL,\n"
" 'size' INTEGER\n"
")");
std::cout << " [Done]" << std::endl;
std::cout << "Started!" << std::endl;
std::cout << "Registered paths: " << std::endl;
auto handlers = drogon::app().getHandlersInfo();
for (const auto& handler : handlers) {
std::cout << " ";
if (std::get<1>(handler) == drogon::HttpMethod::Post) std::cout << "POST ";
else std::cout << "GET ";
std::string func = std::get<2>(handler).substr(16);
func.resize(30, ' ');
std::cout << '[' << func << "] ";
std::cout << std::get<0>(handler) << std::endl;
}
std::cout << "Listening on:" << std::endl;
auto listeners = drogon::app().getListeners();
for (const auto& listener : listeners) {
std::cout << " " << listener.toIpPort() << std::endl;
}
});
Json::Value access_logger;
access_logger["name"] = "drogon::plugin::AccessLogger";
Json::Value config;
config["plugins"].append(access_logger);
drogon::app()
.setClientMaxBodySize(1024L * 1024L * 1024L * 1024L) // 1 TB
.loadConfigJson(config)
.createDbClient("sqlite3", "", 0, "", "", "", 1, "sqlite.db")
.setCustom404Page(drogon::HttpResponse::newFileResponse("./static/index.html"), false)
.setDocumentRoot("./static")
.setBrStatic(true)
.setStaticFilesCacheTime(0)
.setLogPath("./logs")
.setLogLevel(trantor::Logger::LogLevel::kDebug)
.setIntSignalHandler(cleanup)
.setTermSignalHandler(cleanup)
.addListener("0.0.0.0", 1234)
.setThreadNum(2);
std::cout << "Setup done!" << std::endl;
drogon::app().run();
}

17
backend/vcpkg.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
"name": "backend",
"version-string": "1.0.0",
"dependencies": [
{
"name": "drogon",
"features": ["orm", "sqlite3"]
},
"jwt-cpp",
"botan",
"curl",
"pngpp",
"nayuki-qr-code-generator",
"libpng"
]
}

View File

@ -1,8 +0,0 @@
export * as Requests from './requests';
export * as Responses from './responses';
export {
UserRole,
validateSync,
validateAsync,
validateAsyncInline
} from './utils';

View File

@ -1,17 +0,0 @@
import { BaseRequest } from './base';
import { IsEnum, IsNumber } from 'class-validator';
import { UserRole } from '../utils';
class AdminRequest extends BaseRequest {
@IsNumber()
user: number;
}
export class SetUserRole extends AdminRequest {
@IsEnum(UserRole)
role: UserRole;
}
export class LogoutAll extends AdminRequest {}
export class DeleteUser extends AdminRequest {}
export class DisableTfa extends AdminRequest {}

View File

@ -1,50 +0,0 @@
import { BaseRequest } from './base';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString
} from 'class-validator';
export class SignUpRequest extends BaseRequest {
@IsEmail()
username: string;
@IsNotEmpty()
@IsString()
password: string;
}
export class LoginRequest extends SignUpRequest {
@IsOptional()
@IsNotEmpty()
@IsString()
otp?: string;
}
export class TfaSetup extends BaseRequest {
@IsNotEmpty()
@IsBoolean()
mail: boolean;
}
export class TfaComplete extends BaseRequest {
@IsNotEmpty()
@IsBoolean()
mail: boolean;
@IsNotEmpty()
@IsString()
code: string;
}
export class ChangePasswordRequest extends BaseRequest {
@IsNotEmpty()
@IsString()
oldPassword: string;
@IsNotEmpty()
@IsString()
newPassword: string;
}

View File

@ -1,20 +0,0 @@
import { BaseRequest } from './base';
import { IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
export class CreateFolderRequest extends BaseRequest {
@IsInt()
@Min(1)
parent: number;
@IsNotEmpty()
@IsString()
name: string;
}
export class DeleteRequest extends BaseRequest {
@IsInt()
@Min(1)
node: number;
}
export class CreateFileRequest extends CreateFolderRequest {}

View File

@ -1,4 +0,0 @@
export * from './base';
export * as Auth from './auth';
export * as FS from './fs';
export * as Admin from './admin';

View File

@ -1,61 +0,0 @@
import { SuccessResponse } from './base';
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsNumber,
IsString,
ValidateNested
} from 'class-validator';
import { UserRole, ValidateConstructor } from '../utils';
@ValidateConstructor
export class GetUsersEntry {
constructor(
id: number,
gitlab: boolean,
name: string,
role: UserRole,
tfaEnabled: boolean
) {
this.id = id;
this.gitlab = gitlab;
this.name = name;
this.role = role;
this.tfaEnabled = tfaEnabled;
}
@IsNumber()
id: number;
@IsBoolean()
gitlab: boolean;
@IsString()
@IsNotEmpty()
name: string;
@IsEnum(UserRole)
role: UserRole;
@IsBoolean()
tfaEnabled: boolean;
}
@ValidateConstructor
export class GetUsers extends SuccessResponse {
constructor(users: GetUsersEntry[]) {
super();
this.users = users;
}
@IsArray()
@ValidateNested({ each: true })
users: GetUsersEntry[];
}
export class LogoutAllUser extends SuccessResponse {}
export class DeleteUser extends SuccessResponse {}
export class SetUserRole extends SuccessResponse {}
export class DisableTfa extends SuccessResponse {}

View File

@ -1,25 +0,0 @@
import { IsNumber, Max, Min } from 'class-validator';
export class BaseResponse {
constructor(statusCode: number) {
this.statusCode = statusCode;
}
@IsNumber()
@Min(100)
@Max(599)
statusCode: number;
}
export class SuccessResponse extends BaseResponse {
constructor() {
super(200);
}
declare statusCode: 200;
}
export class ErrorResponse extends BaseResponse {
declare statusCode: 400 | 401 | 403;
message?: string;
}

View File

@ -1,89 +0,0 @@
import { SuccessResponse } from './base';
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Min
} from 'class-validator';
import { ValidateConstructor } from '../utils';
@ValidateConstructor
export class GetRootResponse extends SuccessResponse {
constructor(rootId: number) {
super();
this.rootId = rootId;
}
@IsInt()
@Min(1)
rootId: number;
}
export class GetNodeResponse extends SuccessResponse {
constructor(
id: number,
name: string,
isFile: boolean,
parent: number | null
) {
super();
this.id = id;
this.name = name;
this.isFile = isFile;
this.parent = parent;
}
@IsInt()
@Min(1)
id: number;
@IsString()
name: string;
@IsBoolean()
isFile: boolean;
@IsOptional()
@IsInt()
@Min(1)
parent: number | null;
@IsOptional()
@IsInt({ each: true })
@Min(1, { each: true })
children?: number[];
@IsOptional()
@IsInt()
@Min(0)
size?: number;
}
@ValidateConstructor
export class GetPathResponse extends SuccessResponse {
constructor(path: string) {
super();
this.path = path;
}
@IsNotEmpty()
@IsString()
path: string;
}
@ValidateConstructor
export class CreateFolderResponse extends SuccessResponse {
constructor(id: number) {
super();
this.id = id;
}
@IsInt()
@Min(1)
id: number;
}
export class UploadFileResponse extends SuccessResponse {}
export class DeleteResponse extends SuccessResponse {}
export class CreateFileResponse extends CreateFolderResponse {}

View File

@ -1,5 +0,0 @@
export * from './base';
export * as Auth from './auth';
export * as FS from './fs';
export * as User from './user';
export * as Admin from './admin';

View File

@ -1,27 +0,0 @@
import { SuccessResponse } from './base';
import { ValidateConstructor } from '../utils';
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
@ValidateConstructor
export class UserInfoResponse extends SuccessResponse {
constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
super();
this.name = name;
this.gitlab = gitlab;
this.tfaEnabled = tfaEnabled;
}
@IsNotEmpty()
@IsString()
name: string;
@IsBoolean()
gitlab: boolean;
@IsBoolean()
tfaEnabled: boolean;
}
export class DeleteUserResponse extends SuccessResponse {}
export class ChangePasswordResponse extends SuccessResponse {}
export class LogoutAllResponse extends SuccessResponse {}

View File

@ -1,41 +0,0 @@
import { validate, validateSync as _validateSync } from 'class-validator';
export enum UserRole {
ADMIN = 2,
USER = 1,
DISABLED = 0
}
export function validateSync<T extends object>(data: T): void {
const errors = _validateSync(data);
if (errors.length > 0) {
console.error('Validation failed, errors: ', errors);
throw new Error('Validation failed');
}
}
export async function validateAsync<T extends object>(data: T): Promise<void> {
const errors = await validate(data);
if (errors.length > 0) {
console.error('Validation failed, errors: ', errors);
throw new Error('Validation failed');
}
}
export async function validateAsyncInline<T extends object>(
data: T
): Promise<T> {
await validateAsync(data);
return data;
}
export function ValidateConstructor<T extends { new (...args: any[]): any }>(
constr: T
) {
return class extends constr {
constructor(...args: any[]) {
super(...args);
validateSync(this);
}
};
}

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ['@vue/cli-plugin-babel/preset'] presets: ["@vue/cli-plugin-babel/preset"],
}; };

View File

@ -9,6 +9,8 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"filesize": "^9.0.11", "filesize": "^9.0.11",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
@ -27,8 +29,6 @@
"@vue/cli-plugin-typescript": "~5.0.0", "@vue/cli-plugin-typescript": "~5.0.0",
"@vue/cli-service": "~5.0.0", "@vue/cli-service": "~5.0.0",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/eslint-config-typescript": "^9.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",

View File

@ -1,62 +1,62 @@
<script setup async lang="ts"> <script setup async lang="ts">
import { provide, ref } from 'vue'; import { provide, ref } from "vue";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
import { TokenInjectType } from '@/api'; import { TokenInjectType } from "@/api";
const router = useRouter(); const router = useRouter();
const jwt = ref<string | null>(localStorage.getItem('token')); const jwt = ref<string | null>(localStorage.getItem("token"));
function setToken(token: string) { function setToken(token: string) {
jwt.value = token; jwt.value = token;
localStorage.setItem('token', token); localStorage.setItem("token", token);
} }
function logout() { function logout() {
jwt.value = null; jwt.value = null;
localStorage.removeItem('token'); localStorage.removeItem("token");
router.push({ name: 'login' }); router.push({ name: "login" });
} }
provide<TokenInjectType>('jwt', { provide<TokenInjectType>("jwt", {
jwt, jwt,
setToken, setToken,
logout logout,
}); });
</script> </script>
<template> <template>
<nav> <nav>
<template v-if="jwt != null"> <template v-if="jwt != null">
<router-link to="/">Files</router-link> <router-link to="/">Files</router-link>
<span style="margin-left: 2em" /> <span style="margin-left: 2em" />
<router-link to="/profile">Profile</router-link> <router-link to="/profile">Profile</router-link>
<span style="margin-left: 2em" /> <span style="margin-left: 2em" />
<router-link to="/login" @click="logout()">Logout</router-link> <router-link to="/login" @click="logout()">Logout</router-link>
</template> </template>
</nav> </nav>
<router-view /> <router-view />
</template> </template>
<style lang="scss"> <style lang="scss">
#app { #app {
font-family: Avenir, Helvetica, Arial, sans-serif; font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
text-align: center; text-align: center;
color: #2c3e50; color: #2c3e50;
} }
nav { nav {
padding: 30px; padding: 30px;
a { a {
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
&.router-link-exact-active { &.router-link-exact-active {
color: #42b983; color: #42b983;
} }
} }
} }
</style> </style>

View File

@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import App from './App'; import App from "./App";
</script> </script>
<template> <template>
<Suspense> <Suspense>
<App></App> <App></App>
<template #fallback> <template #fallback>
<div>Loading...</div> <div>Loading...</div>
</template> </template>
</Suspense> </Suspense>
</template> </template>

View File

@ -1,54 +1,54 @@
import { Requests, Responses, UserRole, get_token, post_token } from './base'; import { Requests, Responses, UserRole, get_token, post_token } from "./base";
export const get_users = (token: string): Promise<Responses.Admin.GetUsers> => export const get_users = (token: string): Promise<Responses.Admin.GetUsers> =>
get_token('/api/admin/users', token); get_token("/api/admin/users", token);
export const set_role = ( export const set_role = (
user: number, user: number,
role: UserRole, role: UserRole,
token: string token: string
): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> => ): Promise<Responses.Admin.SetUserRole | Responses.ErrorResponse> =>
post_token<Requests.Admin.SetUserRole>( post_token<Requests.Admin.SetUserRole>(
'/api/admin/set_role', "/api/admin/set_role",
{ {
user, user,
role role,
}, },
token token
); );
export const logout = ( export const logout = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> => ): Promise<Responses.Admin.LogoutAllUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.LogoutAll>( post_token<Requests.Admin.LogoutAll>(
'/api/admin/logout', "/api/admin/logout",
{ {
user user,
}, },
token token
); );
export const delete_user = ( export const delete_user = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> => ): Promise<Responses.Admin.DeleteUser | Responses.ErrorResponse> =>
post_token<Requests.Admin.DeleteUser>( post_token<Requests.Admin.DeleteUser>(
'/api/admin/delete', "/api/admin/delete",
{ {
user user,
}, },
token token
); );
export const disable_tfa = ( export const disable_tfa = (
user: number, user: number,
token: string token: string
): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> => ): Promise<Responses.Admin.DisableTfa | Responses.ErrorResponse> =>
post_token<Requests.Admin.DisableTfa>( post_token<Requests.Admin.DisableTfa>(
'/api/admin/disable_2fa', "/api/admin/disable_2fa",
{ {
user user,
}, },
token token
); );

View File

@ -1,93 +1,93 @@
import { Responses, Requests, post, post_token } from './base'; import { Responses, Requests, post, post_token } from "./base";
export const auth_login = ( export const auth_login = (
username: string, username: string,
password: string, password: string,
otp?: string otp?: string
): Promise< ): Promise<
| Responses.Auth.LoginResponse | Responses.Auth.LoginResponse
| Responses.Auth.TfaRequiredResponse | Responses.Auth.TfaRequiredResponse
| Responses.ErrorResponse | Responses.ErrorResponse
> => > =>
post<Requests.Auth.LoginRequest>('/api/auth/login', { post<Requests.Auth.LoginRequest>("/api/auth/login", {
username: username, username: username,
password: password, password: password,
otp: otp otp: otp,
}); });
export const auth_signup = ( export const auth_signup = (
username: string, username: string,
password: string password: string
): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.SignupResponse | Responses.ErrorResponse> =>
post<Requests.Auth.SignUpRequest>('/api/auth/signup', { post<Requests.Auth.SignUpRequest>("/api/auth/signup", {
username: username, username: username,
password: password password: password,
}); });
export const refresh_token = ( export const refresh_token = (
token: string token: string
): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.RefreshResponse | Responses.ErrorResponse> =>
post_token('/api/auth/refresh', {}, token); post_token("/api/auth/refresh", {}, token);
export const change_password = ( export const change_password = (
oldPw: string, oldPw: string,
newPw: string, newPw: string,
token: string token: string
): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.ChangePasswordResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.ChangePasswordRequest>( post_token<Requests.Auth.ChangePasswordRequest>(
'/api/auth/change_password', "/api/auth/change_password",
{ {
oldPassword: oldPw, oldPassword: oldPw,
newPassword: newPw newPassword: newPw,
}, },
token token
); );
export const logout_all = ( export const logout_all = (
token: string token: string
): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.LogoutAllResponse | Responses.ErrorResponse> =>
post_token('/api/auth/logout_all', {}, token); post_token("/api/auth/logout_all", {}, token);
export function tfa_setup( export function tfa_setup(
mail: false, mail: false,
token: string token: string
): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>; ): Promise<Responses.Auth.RequestTotpTfaResponse | Responses.ErrorResponse>;
export function tfa_setup( export function tfa_setup(
mail: true, mail: true,
token: string token: string
): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>; ): Promise<Responses.Auth.RequestEmailTfaResponse | Responses.ErrorResponse>;
export function tfa_setup( export function tfa_setup(
mail: boolean, mail: boolean,
token: string token: string
): Promise< ): Promise<
| Responses.Auth.RequestEmailTfaResponse | Responses.Auth.RequestEmailTfaResponse
| Responses.Auth.RequestTotpTfaResponse | Responses.Auth.RequestTotpTfaResponse
| Responses.ErrorResponse | Responses.ErrorResponse
> { > {
return post_token<Requests.Auth.TfaSetup>( return post_token<Requests.Auth.TfaSetup>(
'/api/auth/2fa/setup', "/api/auth/2fa/setup",
{ {
mail mail,
}, },
token token
); );
} }
export const tfa_complete = ( export const tfa_complete = (
mail: boolean, mail: boolean,
code: string, code: string,
token: string token: string
): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.TfaCompletedResponse | Responses.ErrorResponse> =>
post_token<Requests.Auth.TfaComplete>( post_token<Requests.Auth.TfaComplete>(
'/api/auth/2fa/complete', "/api/auth/2fa/complete",
{ {
mail, mail,
code code,
}, },
token token
); );
export const tfa_disable = ( export const tfa_disable = (
token: string token: string
): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> => ): Promise<Responses.Auth.RemoveTfaResponse | Responses.ErrorResponse> =>
post_token('/api/auth/2fa/disable', {}, token); post_token("/api/auth/2fa/disable", {}, token);

View File

@ -1,62 +1,62 @@
import axios from 'axios'; import axios from "axios";
import { Requests, Responses, UserRole } from '../../../dto'; import { Requests, Responses, UserRole } from "../dto";
export { Requests, Responses, UserRole }; export { Requests, Responses, UserRole };
export const post = <T extends Requests.BaseRequest>(url: string, data: T) => export const post = <T extends Requests.BaseRequest>(url: string, data: T) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { 'Content-type': 'application/json' } headers: { "Content-type": "application/json" },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const post_token = <T extends Requests.BaseRequest>( export const post_token = <T extends Requests.BaseRequest>(
url: string, url: string,
data: T, data: T,
token: string token: string
) => ) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { headers: {
Authorization: 'Bearer ' + token, Authorization: "Bearer " + token,
'Content-type': 'application/json' "Content-type": "application/json",
} },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const post_token_form = ( export const post_token_form = (
url: string, url: string,
data: FormData, data: FormData,
token: string, token: string,
onProgress: (progressEvent: ProgressEvent) => void onProgress: (progressEvent: ProgressEvent) => void
) => ) =>
axios axios
.post(url, data, { .post(url, data, {
headers: { headers: {
Authorization: 'Bearer ' + token, Authorization: "Bearer " + token,
'Content-type': 'multipart/form-data' "Content-type": "multipart/form-data",
}, },
onUploadProgress: onProgress onUploadProgress: onProgress,
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const get = (url: string) => export const get = (url: string) =>
axios axios
.get(url) .get(url)
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const get_token = (url: string, token: string) => export const get_token = (url: string, token: string) =>
axios axios
.get(url, { .get(url, {
headers: { Authorization: 'Bearer ' + token } headers: { Authorization: "Bearer " + token },
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((err) => err.response.data); .catch((err) => err.response.data);
export const isErrorResponse = ( export const isErrorResponse = (
res: Responses.BaseResponse res: Responses.BaseResponse
): res is Responses.ErrorResponse => res.statusCode != 200; ): res is Responses.ErrorResponse => res.statusCode != 200;

View File

@ -1,95 +1,84 @@
import { import {
Responses, Responses,
Requests, Requests,
get_token, get_token,
post_token, post_token,
post_token_form, post_token_form,
isErrorResponse isErrorResponse,
} from './base'; } from "./base";
export const get_root = ( export const get_root = (
token: string token: string
): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetRootResponse | Responses.ErrorResponse> =>
get_token('/api/fs/root', token); get_token("/api/fs/root", token);
export const get_node = ( export const get_node = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetNodeResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/node/${node}`, token); get_token(`/api/fs/node/${node}`, token);
export const get_path = ( export const get_path = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.GetPathResponse | Responses.ErrorResponse> =>
get_token(`/api/fs/path/${node}`, token); get_token(`/api/fs/path/${node}`, token);
export const create_folder = ( export const create_folder = (
token: string, token: string,
parent: number, parent: number,
name: string name: string
): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.CreateFolderResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFolderRequest>( post_token<Requests.FS.CreateFolderRequest>(
'/api/fs/createFolder', "/api/fs/createFolder",
{ {
parent: parent, parent: parent,
name: name name: name,
}, },
token token
); );
export const create_file = ( export const create_file = (
token: string, token: string,
parent: number, parent: number,
name: string name: string
): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.CreateFileResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.CreateFileRequest>( post_token<Requests.FS.CreateFileRequest>(
'/api/fs/createFile', "/api/fs/createFile",
{ {
parent: parent, parent: parent,
name: name name: name,
}, },
token token
); );
export const delete_node = ( export const delete_node = (
token: string, token: string,
node: number node: number
): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> => ): Promise<Responses.FS.DeleteResponse | Responses.ErrorResponse> =>
post_token<Requests.FS.DeleteRequest>( post_token(`/api/fs/delete/${node}`, {}, token);
'/api/fs/delete',
{
node: node
},
token
);
export const upload_file = async ( export const upload_file = async (
token: string, token: string,
parent: number, parent: number,
file: File, file: File,
onProgress: (progressEvent: ProgressEvent) => void onProgress: (progressEvent: ProgressEvent) => void
): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => { ): Promise<Responses.FS.UploadFileResponse | Responses.ErrorResponse> => {
const node = await create_file(token, parent, file.name); const node = await create_file(token, parent, file.name);
if (isErrorResponse(node)) return node; if (isErrorResponse(node)) return node;
const form = new FormData(); const form = new FormData();
form.set('file', file); form.set("file", file);
return post_token_form( return post_token_form(`/api/fs/upload/${node.id}`, form, token, onProgress);
`/api/fs/upload/${node.id}`,
form,
token,
onProgress
);
}; };
export function download_file(token: string, id: number) { export function download_file(token: string, id: number) {
const form = document.createElement('form'); const form = document.createElement("form");
form.method = 'post'; form.method = "post";
form.target = '_blank'; form.target = "_blank";
form.action = '/api/fs/download'; form.action = "/api/fs/download";
form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`; form.innerHTML = `<input type="hidden" name="jwtToken" value="${token}"><input type="hidden" name="id" value="${id}">`;
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
document.body.removeChild(form); document.body.removeChild(form);
} }

View File

@ -1,6 +1,6 @@
export { Requests, Responses, UserRole, isErrorResponse } from './base'; export { Requests, Responses, UserRole, isErrorResponse } from "./base";
export * as Auth from './auth'; export * as Auth from "./auth";
export * as FS from './fs'; export * as FS from "./fs";
export * as User from './user'; export * as User from "./user";
export * as Admin from './admin'; export * as Admin from "./admin";
export * from './util'; export * from "./util";

View File

@ -1,11 +1,11 @@
import { Responses, get_token, post_token } from '@/api/base'; import { Responses, get_token, post_token } from "@/api/base";
export const get_user_info = ( export const get_user_info = (
token: string token: string
): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> => ): Promise<Responses.User.UserInfoResponse | Responses.ErrorResponse> =>
get_token('/api/user/info', token); get_token("/api/user/info", token);
export const delete_user = ( export const delete_user = (
token: string token: string
): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> => ): Promise<Responses.User.DeleteUserResponse | Responses.ErrorResponse> =>
post_token('/api/user/delete', {}, token); post_token("/api/user/delete", {}, token);

View File

@ -1,25 +1,25 @@
import jwtDecode, { JwtPayload } from 'jwt-decode'; import jwtDecode, { JwtPayload } from "jwt-decode";
import { Ref, UnwrapRef } from 'vue'; import { Ref, UnwrapRef } from "vue";
import { isErrorResponse } from './base'; import { isErrorResponse } from "./base";
import { refresh_token } from './auth'; import { refresh_token } from "./auth";
export async function check_token( export async function check_token(
token: TokenInjectType token: TokenInjectType
): Promise<string | void> { ): Promise<string | void> {
if (!token.jwt.value) return token.logout(); if (!token.jwt.value) return token.logout();
const payload = jwtDecode<JwtPayload>(token.jwt.value); const payload = jwtDecode<JwtPayload>(token.jwt.value);
if (!payload) return token.logout(); if (!payload) return token.logout();
// Expires in more than 60 Minute // Expires in more than 60 Minute
if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60)) if (payload.exp && payload.exp > Math.floor(Date.now() / 1000 + 60 * 60))
return token.jwt.value; return token.jwt.value;
const new_token = await refresh_token(token.jwt.value); const new_token = await refresh_token(token.jwt.value);
if (isErrorResponse(new_token)) return token.logout(); if (isErrorResponse(new_token)) return token.logout();
token.setToken(new_token.jwt); token.setToken(new_token.jwt);
return new_token.jwt; return new_token.jwt;
} }
export type TokenInjectType = { export type TokenInjectType = {
jwt: Ref<UnwrapRef<string | null>>; jwt: Ref<UnwrapRef<string | null>>;
setToken: (token: string) => void; setToken: (token: string) => void;
logout: () => void; logout: () => void;
}; };

View File

@ -1,40 +1,40 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps, inject } from 'vue'; import { defineEmits, defineProps, inject } from "vue";
import { check_token, FS, Responses, TokenInjectType } from '@/api'; import { check_token, FS, Responses, TokenInjectType } from "@/api";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'reloadNode'): void; (e: "reloadNode"): void;
}>(); }>();
async function del() { async function del() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.delete_node(token, props.node.id); await FS.delete_node(token, props.node.id);
emit('reloadNode'); emit("reloadNode");
} }
async function download() { async function download() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
FS.download_file(token, props.node.id); FS.download_file(token, props.node.id);
} }
</script> </script>
<template> <template>
<td> <td>
<router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link> <router-link :to="'/fs/' + props.node.id">{{ node.name }}</router-link>
</td> </td>
<td> <td>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a> <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</td> </td>
<td> <td>
<a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a> <a href="#" @click="del()" v-if="props.node.name !== '..'">delete</a>
</td> </td>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,108 +1,101 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps, inject, reactive, ref, watch } from 'vue'; import { defineEmits, defineProps, inject, reactive, ref, watch } from "vue";
import { FS, Responses, check_token, TokenInjectType } from '@/api'; import { FS, Responses, check_token, TokenInjectType } from "@/api";
import DirEntry from '@/components/FSView/DirEntry.vue'; import DirEntry from "@/components/FSView/DirEntry.vue";
import UploadFileDialog from '@/components/UploadDialog/UploadFileDialog.vue'; import UploadFileDialog from "@/components/UploadDialog/UploadFileDialog.vue";
import { NModal } from 'naive-ui'; import { NModal } from "naive-ui";
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'reloadNode'): void; (e: "reloadNode"): void;
(e: 'gotoRoot'): void; (e: "gotoRoot"): void;
}>(); }>();
const fileInput = ref<HTMLInputElement>(); const fileInput = ref<HTMLInputElement>();
const uploadDialog = ref(); const uploadDialog = ref();
const uploadDialogShow = ref(false); const uploadDialogShow = ref(false);
const new_folder_name = ref(''); const new_folder_name = ref("");
const files = ref<File[]>([]); const files = ref<File[]>([]);
const nodes = ref<Responses.FS.GetNodeResponse[]>([]); const nodes = ref<Responses.FS.GetNodeResponse[]>([]);
const hasParent = ref(false); const hasParent = ref(false);
const parentNode = reactive<Responses.FS.GetNodeResponse>({ const parentNode = reactive<Responses.FS.GetNodeResponse>({
id: 0, id: 0,
statusCode: 200, statusCode: 200,
isFile: false, isFile: false,
parent: null, parent: null,
name: '..' name: "..",
}); });
watch( watch(
() => props.node, () => props.node,
async (to) => { async (to) => {
parentNode.id = to.parent ?? 0; parentNode.id = to.parent ?? 0;
hasParent.value = to.parent != null; hasParent.value = to.parent != null;
nodes.value = []; nodes.value = [];
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Promise.all( await Promise.all(
to.children?.map(async (child) => { to.children?.map(async (child) => {
nodes.value.push( nodes.value.push(
(await FS.get_node( (await FS.get_node(token, child)) as Responses.FS.GetNodeResponse
token, );
child }) ?? []
)) as Responses.FS.GetNodeResponse );
); },
}) ?? [] { immediate: true }
);
},
{ immediate: true }
); );
async function newFolder() { async function newFolder() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.create_folder(token, props.node.id, new_folder_name.value); await FS.create_folder(token, props.node.id, new_folder_name.value);
emit('reloadNode'); emit("reloadNode");
} }
async function uploadFiles() { async function uploadFiles() {
files.value = Array.from(fileInput.value?.files ?? []); files.value = Array.from(fileInput.value?.files ?? []);
if (files.value.length == 0) return; if (files.value.length == 0) return;
uploadDialogShow.value = true; uploadDialogShow.value = true;
} }
async function uploadFilesDialogOpen() { async function uploadFilesDialogOpen() {
await uploadDialog.value?.startUpload(props.node.id); await uploadDialog.value?.startUpload(props.node.id);
uploadDialogShow.value = false; uploadDialogShow.value = false;
if (fileInput.value) fileInput.value.value = ''; if (fileInput.value) fileInput.value.value = "";
emit('reloadNode'); emit("reloadNode");
} }
</script> </script>
<template> <template>
<div> <div>
<input <input type="text" placeholder="Folder name" v-model="new_folder_name" />
type="text" <a href="#" @click="newFolder()">create folder</a>
placeholder="Folder name" </div>
v-model="new_folder_name" <div>
/> <input type="file" ref="fileInput" multiple />
<a href="#" @click="newFolder()">create folder</a> <a href="#" @click="uploadFiles()">upload files</a>
</div> </div>
<div> <table>
<input type="file" ref="fileInput" multiple /> <tr v-if="hasParent">
<a href="#" @click="uploadFiles()">upload files</a> <DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" />
</div> </tr>
<table> <tr v-for="n in nodes" :key="n.id">
<tr v-if="hasParent"> <DirEntry :node="n" @reloadNode="emit('reloadNode')" />
<DirEntry :node="parentNode" @reloadNode="emit('reloadNode')" /> </tr>
</tr> </table>
<tr v-for="n in nodes" :key="n.id"> <n-modal
<DirEntry :node="n" @reloadNode="emit('reloadNode')" /> v-model:show="uploadDialogShow"
</tr> :close-on-esc="false"
</table> :mask-closable="false"
<n-modal :on-after-enter="uploadFilesDialogOpen"
v-model:show="uploadDialogShow" >
:close-on-esc="false" <UploadFileDialog ref="uploadDialog" :files="files" />
:mask-closable="false" </n-modal>
:on-after-enter="uploadFilesDialogOpen"
>
<UploadFileDialog ref="uploadDialog" :files="files" />
</n-modal>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,38 +1,38 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, inject } from 'vue'; import { defineProps, inject } from "vue";
import { check_token, FS, Responses, TokenInjectType } from '@/api'; import { check_token, FS, Responses, TokenInjectType } from "@/api";
const props = defineProps<{ const props = defineProps<{
node: Responses.FS.GetNodeResponse; node: Responses.FS.GetNodeResponse;
}>(); }>();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function del() { async function del() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await FS.delete_node(token, props.node.id); await FS.delete_node(token, props.node.id);
} }
async function download() { async function download() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
FS.download_file(token, props.node.id); FS.download_file(token, props.node.id);
} }
</script> </script>
<template> <template>
<div> <div>
<router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link> <router-link :to="'/fs/' + props.node.parent ?? 0">..</router-link>
</div> </div>
<div> <div>
<a href="#" @click="download()" v-if="props.node.isFile">Download</a> <a href="#" @click="download()" v-if="props.node.isFile">Download</a>
</div> </div>
<div> <div>
<router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()"> <router-link :to="'/fs/' + props.node.parent ?? 0" @click="del()">
delete delete
</router-link> </router-link>
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,156 +1,140 @@
<template> <template>
<div class="hello"> <div class="hello">
<h1>{{ msg }}</h1> <h1>{{ msg }}</h1>
<p> <p>
For a guide and recipes on how to configure / customize this For a guide and recipes on how to configure / customize this project,<br />
project,<br /> check out the
check out the <a href="https://cli.vuejs.org" target="_blank" rel="noopener"
<a href="https://cli.vuejs.org" target="_blank" rel="noopener" >vue-cli documentation</a
>vue-cli documentation</a >.
>. </p>
</p> <h3>Installed CLI Plugins</h3>
<h3>Installed CLI Plugins</h3> <ul>
<ul> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >babel</a
>babel</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >router</a
>router</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >vuex</a
>vuex</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >eslint</a
>eslint</a >
> </li>
</li> <li>
<li> <a
<a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript"
href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank"
target="_blank" rel="noopener"
rel="noopener" >typescript</a
>typescript</a >
> </li>
</li> </ul>
</ul> <h3>Essential Links</h3>
<h3>Essential Links</h3> <ul>
<ul> <li>
<li> <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
<a href="https://vuejs.org" target="_blank" rel="noopener" </li>
>Core Docs</a <li>
> <a href="https://forum.vuejs.org" target="_blank" rel="noopener"
</li> >Forum</a
<li> >
<a href="https://forum.vuejs.org" target="_blank" rel="noopener" </li>
>Forum</a <li>
> <a href="https://chat.vuejs.org" target="_blank" rel="noopener"
</li> >Community Chat</a
<li> >
<a href="https://chat.vuejs.org" target="_blank" rel="noopener" </li>
>Community Chat</a <li>
> <a href="https://twitter.com/vuejs" target="_blank" rel="noopener"
</li> >Twitter</a
<li> >
<a </li>
href="https://twitter.com/vuejs" <li>
target="_blank" <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
rel="noopener" </li>
>Twitter</a </ul>
> <h3>Ecosystem</h3>
</li> <ul>
<li> <li>
<a href="https://news.vuejs.org" target="_blank" rel="noopener" <a href="https://router.vuejs.org" target="_blank" rel="noopener"
>News</a >vue-router</a
> >
</li> </li>
</ul> <li>
<h3>Ecosystem</h3> <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
<ul> </li>
<li> <li>
<a <a
href="https://router.vuejs.org" href="https://github.com/vuejs/vue-devtools#vue-devtools"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
>vue-router</a >vue-devtools</a
> >
</li> </li>
<li> <li>
<a href="https://vuex.vuejs.org" target="_blank" rel="noopener" <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener"
>vuex</a >vue-loader</a
> >
</li> </li>
<li> <li>
<a <a
href="https://github.com/vuejs/vue-devtools#vue-devtools" href="https://github.com/vuejs/awesome-vue"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
>vue-devtools</a >awesome-vue</a
> >
</li> </li>
<li> </ul>
<a </div>
href="https://vue-loader.vuejs.org"
target="_blank"
rel="noopener"
>vue-loader</a
>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
rel="noopener"
>awesome-vue</a
>
</li>
</ul>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
name: 'HelloWorld', name: "HelloWorld",
props: { props: {
msg: String msg: String,
} },
}); });
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss"> <style scoped lang="scss">
h3 { h3 {
margin: 40px 0 0; margin: 40px 0 0;
} }
ul { ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
} }
li { li {
display: inline-block; display: inline-block;
margin: 0 10px; margin: 0 10px;
} }
a { a {
color: #42b983; color: #42b983;
} }
</style> </style>

View File

@ -1,51 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineExpose, ref } from 'vue'; import { defineProps, defineExpose, ref } from "vue";
import { isErrorResponse, FS } from '@/api'; import { isErrorResponse, FS } from "@/api";
import { NProgress } from 'naive-ui'; import { NProgress } from "naive-ui";
import filesize from 'filesize'; import filesize from "filesize";
const props = defineProps<{ const props = defineProps<{
file: File; file: File;
}>(); }>();
const progress = ref(0); const progress = ref(0);
const percentage = ref(0); const percentage = ref(0);
const err = ref(''); const err = ref("");
const status = ref('info'); const status = ref("info");
async function startUpload(parent: number, token: string) { async function startUpload(parent: number, token: string) {
const resp = await FS.upload_file(token, parent, props.file, (e) => { const resp = await FS.upload_file(token, parent, props.file, (e) => {
progress.value = e.loaded; progress.value = e.loaded;
percentage.value = (e.loaded / e.total) * 100; percentage.value = (e.loaded / e.total) * 100;
}); });
percentage.value = 100; percentage.value = 100;
if (isErrorResponse(resp)) { if (isErrorResponse(resp)) {
err.value = resp.message ?? 'Error'; err.value = resp.message ?? "Error";
status.value = 'error'; status.value = "error";
} else status.value = 'success'; } else status.value = "success";
} }
defineExpose({ defineExpose({
startUpload startUpload,
}); });
</script> </script>
<template> <template>
<div v-if="percentage < 100"> <div v-if="percentage < 100">
{{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} - {{ file.name }} - {{ filesize(progress) }} / {{ filesize(file.size) }} -
{{ Math.floor(percentage * 1000) / 1000 }}% {{ Math.floor(percentage * 1000) / 1000 }}%
</div> </div>
<div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div> <div v-else-if="err !== ''">{{ file.name }} - Error: {{ err }}</div>
<div v-else>{{ file.name }} - Completed</div> <div v-else>{{ file.name }} - Completed</div>
<n-progress <n-progress
type="line" type="line"
:percentage="percentage" :percentage="percentage"
:height="20" :height="20"
:status="status" :status="status"
border-radius="10px 0" border-radius="10px 0"
fill-border-radius="10px 0" fill-border-radius="10px 0"
:show-indicator="false" :show-indicator="false"
/> />
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineExpose, ref, inject } from 'vue'; import { defineProps, defineExpose, ref, inject } from "vue";
import { check_token, TokenInjectType } from '@/api'; import { check_token, TokenInjectType } from "@/api";
import UploadEntry from '@/components/UploadDialog/UploadEntry.vue'; import UploadEntry from "@/components/UploadDialog/UploadEntry.vue";
import { NCard } from 'naive-ui'; import { NCard } from "naive-ui";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const entries = ref<typeof UploadEntry[]>([]); const entries = ref<typeof UploadEntry[]>([]);
const done = ref(false); const done = ref(false);
@ -12,37 +12,32 @@ let canCloseResolve = null;
const canClose = new Promise((r) => (canCloseResolve = r)); const canClose = new Promise((r) => (canCloseResolve = r));
async function startUpload(parent: number) { async function startUpload(parent: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Promise.all( await Promise.all(
entries.value.map((entry) => entry.startUpload(parent, token)) entries.value.map((entry) => entry.startUpload(parent, token))
); );
done.value = true; done.value = true;
await canClose; await canClose;
} }
defineExpose({ defineExpose({
startUpload startUpload,
}); });
defineProps<{ defineProps<{
files: File[]; files: File[];
}>(); }>();
</script> </script>
<template> <template>
<n-card title="Upload Files"> <n-card title="Upload Files">
<div> <div>
<UploadEntry <UploadEntry v-for="f in files" :key="f.name" ref="entries" :file="f" />
v-for="f in files" </div>
:key="f.name" <div>
ref="entries" <button v-if="done" @click="canCloseResolve()">Close</button>
:file="f" </div>
/> </n-card>
</div>
<div>
<button v-if="done" @click="canCloseResolve()">Close</button>
</div>
</n-card>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -0,0 +1,8 @@
export * as Requests from "./requests";
export * as Responses from "./responses";
export {
UserRole,
validateSync,
validateAsync,
validateAsyncInline,
} from "./utils";

View File

@ -0,0 +1,17 @@
import { BaseRequest } from "./base";
import { IsEnum, IsNumber } from "class-validator";
import { UserRole } from "../utils";
export class AdminRequest extends BaseRequest {
@IsNumber()
user: number;
}
export class SetUserRole extends AdminRequest {
@IsEnum(UserRole)
role: UserRole;
}
export class LogoutAll extends AdminRequest {}
export class DeleteUser extends AdminRequest {}
export class DisableTfa extends AdminRequest {}

View File

@ -0,0 +1,50 @@
import { BaseRequest } from "./base";
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
} from "class-validator";
export class SignUpRequest extends BaseRequest {
@IsEmail()
username: string;
@IsNotEmpty()
@IsString()
password: string;
}
export class LoginRequest extends SignUpRequest {
@IsOptional()
@IsNotEmpty()
@IsString()
otp?: string;
}
export class TfaSetup extends BaseRequest {
@IsNotEmpty()
@IsBoolean()
mail: boolean;
}
export class TfaComplete extends BaseRequest {
@IsNotEmpty()
@IsBoolean()
mail: boolean;
@IsNotEmpty()
@IsString()
code: string;
}
export class ChangePasswordRequest extends BaseRequest {
@IsNotEmpty()
@IsString()
oldPassword: string;
@IsNotEmpty()
@IsString()
newPassword: string;
}

View File

@ -0,0 +1,14 @@
import { BaseRequest } from "./base";
import { IsInt, IsNotEmpty, IsString, Min } from "class-validator";
export class CreateFolderRequest extends BaseRequest {
@IsInt()
@Min(1)
parent: number;
@IsNotEmpty()
@IsString()
name: string;
}
export class CreateFileRequest extends CreateFolderRequest {}

View File

@ -0,0 +1,4 @@
export * from "./base";
export * as Auth from "./auth";
export * as FS from "./fs";
export * as Admin from "./admin";

View File

@ -0,0 +1,61 @@
import { SuccessResponse } from "./base";
import {
IsArray,
IsBoolean,
IsEnum,
IsNotEmpty,
IsNumber,
IsString,
ValidateNested,
} from "class-validator";
import { UserRole, ValidateConstructor } from "../utils";
@ValidateConstructor
export class GetUsersEntry {
constructor(
id: number,
gitlab: boolean,
name: string,
role: UserRole,
tfaEnabled: boolean
) {
this.id = id;
this.gitlab = gitlab;
this.name = name;
this.role = role;
this.tfaEnabled = tfaEnabled;
}
@IsNumber()
id: number;
@IsBoolean()
gitlab: boolean;
@IsString()
@IsNotEmpty()
name: string;
@IsEnum(UserRole)
role: UserRole;
@IsBoolean()
tfaEnabled: boolean;
}
@ValidateConstructor
export class GetUsers extends SuccessResponse {
constructor(users: GetUsersEntry[]) {
super();
this.users = users;
}
@IsArray()
@ValidateNested({ each: true })
users: GetUsersEntry[];
}
export class LogoutAllUser extends SuccessResponse {}
export class DeleteUser extends SuccessResponse {}
export class SetUserRole extends SuccessResponse {}
export class DisableTfa extends SuccessResponse {}

View File

@ -1,33 +1,33 @@
import { SuccessResponse } from './base'; import { SuccessResponse } from "./base";
import { IsBase32, IsJWT, IsNotEmpty } from 'class-validator'; import { IsBase32, IsJWT, IsNotEmpty } from "class-validator";
import { ValidateConstructor } from '../utils'; import { ValidateConstructor } from "../utils";
@ValidateConstructor @ValidateConstructor
export class LoginResponse extends SuccessResponse { export class LoginResponse extends SuccessResponse {
constructor(jwt: string) { constructor(jwt: string) {
super(); super();
this.jwt = jwt; this.jwt = jwt;
} }
@IsNotEmpty() @IsNotEmpty()
@IsJWT() @IsJWT()
jwt: string; jwt: string;
} }
@ValidateConstructor @ValidateConstructor
export class RequestTotpTfaResponse extends SuccessResponse { export class RequestTotpTfaResponse extends SuccessResponse {
constructor(qrCode: string, secret: string) { constructor(qrCode: string, secret: string) {
super(); super();
this.qrCode = qrCode; this.qrCode = qrCode;
this.secret = secret; this.secret = secret;
} }
@IsNotEmpty() @IsNotEmpty()
qrCode: string; qrCode: string;
@IsNotEmpty() @IsNotEmpty()
@IsBase32() @IsBase32()
secret: string; secret: string;
} }
export class TfaRequiredResponse extends SuccessResponse {} export class TfaRequiredResponse extends SuccessResponse {}

View File

@ -0,0 +1,25 @@
import { IsNumber, Max, Min } from "class-validator";
export class BaseResponse {
constructor(statusCode: number) {
this.statusCode = statusCode;
}
@IsNumber()
@Min(100)
@Max(599)
statusCode: number;
}
export class SuccessResponse extends BaseResponse {
constructor() {
super(200);
}
declare statusCode: 200;
}
export class ErrorResponse extends BaseResponse {
declare statusCode: 400 | 401 | 403;
message?: string;
}

View File

@ -0,0 +1,89 @@
import { SuccessResponse } from "./base";
import {
IsBoolean,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
Min,
} from "class-validator";
import { ValidateConstructor } from "../utils";
@ValidateConstructor
export class GetRootResponse extends SuccessResponse {
constructor(rootId: number) {
super();
this.rootId = rootId;
}
@IsInt()
@Min(1)
rootId: number;
}
export class GetNodeResponse extends SuccessResponse {
constructor(
id: number,
name: string,
isFile: boolean,
parent: number | null
) {
super();
this.id = id;
this.name = name;
this.isFile = isFile;
this.parent = parent;
}
@IsInt()
@Min(1)
id: number;
@IsString()
name: string;
@IsBoolean()
isFile: boolean;
@IsOptional()
@IsInt()
@Min(1)
parent: number | null;
@IsOptional()
@IsInt({ each: true })
@Min(1, { each: true })
children?: number[];
@IsOptional()
@IsInt()
@Min(0)
size?: number;
}
@ValidateConstructor
export class GetPathResponse extends SuccessResponse {
constructor(path: string) {
super();
this.path = path;
}
@IsNotEmpty()
@IsString()
path: string;
}
@ValidateConstructor
export class CreateFolderResponse extends SuccessResponse {
constructor(id: number) {
super();
this.id = id;
}
@IsInt()
@Min(1)
id: number;
}
export class UploadFileResponse extends SuccessResponse {}
export class DeleteResponse extends SuccessResponse {}
export class CreateFileResponse extends CreateFolderResponse {}

View File

@ -0,0 +1,5 @@
export * from "./base";
export * as Auth from "./auth";
export * as FS from "./fs";
export * as User from "./user";
export * as Admin from "./admin";

View File

@ -0,0 +1,27 @@
import { SuccessResponse } from "./base";
import { ValidateConstructor } from "../utils";
import { IsBoolean, IsNotEmpty, IsString } from "class-validator";
@ValidateConstructor
export class UserInfoResponse extends SuccessResponse {
constructor(name: string, gitlab: boolean, tfaEnabled: boolean) {
super();
this.name = name;
this.gitlab = gitlab;
this.tfaEnabled = tfaEnabled;
}
@IsNotEmpty()
@IsString()
name: string;
@IsBoolean()
gitlab: boolean;
@IsBoolean()
tfaEnabled: boolean;
}
export class DeleteUserResponse extends SuccessResponse {}
export class ChangePasswordResponse extends SuccessResponse {}
export class LogoutAllResponse extends SuccessResponse {}

41
frontend/src/dto/utils.ts Normal file
View File

@ -0,0 +1,41 @@
import { validate, validateSync as _validateSync } from "class-validator";
export enum UserRole {
ADMIN = 2,
USER = 1,
DISABLED = 0,
}
export function validateSync<T extends object>(data: T): void {
const errors = _validateSync(data);
if (errors.length > 0) {
console.error("Validation failed, errors: ", errors);
throw new Error("Validation failed");
}
}
export async function validateAsync<T extends object>(data: T): Promise<void> {
const errors = await validate(data);
if (errors.length > 0) {
console.error("Validation failed, errors: ", errors);
throw new Error("Validation failed");
}
}
export async function validateAsyncInline<T extends object>(
data: T
): Promise<T> {
await validateAsync(data);
return data;
}
export function ValidateConstructor<T extends { new (...args: any[]): any }>(
constr: T
) {
return class extends constr {
constructor(...args: any[]) {
super(...args);
validateSync(this);
}
};
}

View File

@ -1,8 +1,8 @@
import { createApp } from 'vue'; import { createApp } from "vue";
import router from './router'; import router from "./router";
import AppAsyncWrapper from './AppAsyncWrapper.vue'; import AppAsyncWrapper from "./AppAsyncWrapper.vue";
const app = createApp(AppAsyncWrapper); const app = createApp(AppAsyncWrapper);
app.use(router); app.use(router);
app.config.unwrapInjectedRef = true; app.config.unwrapInjectedRef = true;
app.mount('#app'); app.mount("#app");

View File

@ -1,63 +1,63 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import LoginView from '@/views/LoginView.vue'; import LoginView from "@/views/LoginView.vue";
import SignupView from '@/views/SignupView.vue'; import SignupView from "@/views/SignupView.vue";
import HomeView from '@/views/HomeView.vue'; import HomeView from "@/views/HomeView.vue";
import AboutView from '@/views/AboutView.vue'; import AboutView from "@/views/AboutView.vue";
import FSView from '@/views/FSView.vue'; import FSView from "@/views/FSView.vue";
import SetTokenView from '@/views/SetTokenView.vue'; import SetTokenView from "@/views/SetTokenView.vue";
import ProfileView from '@/views/ProfileView.vue'; import ProfileView from "@/views/ProfileView.vue";
import TFAView from '@/views/TFAView.vue'; import TFAView from "@/views/TFAView.vue";
import AdminView from '@/views/AdminView.vue'; import AdminView from "@/views/AdminView.vue";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: '/', path: "/",
name: 'home', name: "home",
component: HomeView component: HomeView,
}, },
{ {
path: '/profile', path: "/profile",
name: 'profile', name: "profile",
component: ProfileView component: ProfileView,
}, },
{ {
path: '/profile/2fa-enable', path: "/profile/2fa-enable",
name: '2fa', name: "2fa",
component: TFAView component: TFAView,
}, },
{ {
path: '/admin', path: "/admin",
component: AdminView component: AdminView,
}, },
{ {
path: '/about', path: "/about",
component: AboutView component: AboutView,
}, },
{ {
path: '/login', path: "/login",
name: 'login', name: "login",
component: LoginView component: LoginView,
}, },
{ {
path: '/signup', path: "/signup",
name: 'signup', name: "signup",
component: SignupView component: SignupView,
}, },
{ {
path: '/fs/:node_id', path: "/fs/:node_id",
name: 'fs', name: "fs",
component: FSView component: FSView,
}, },
{ {
path: '/set_token', path: "/set_token",
component: SetTokenView component: SetTokenView,
} },
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(process.env.BASE_URL),
routes routes,
}); });
export default router; export default router;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="about"> <div class="about">
<h1>This is an about page</h1> <h1>This is an about page</h1>
</div> </div>
</template> </template>

View File

@ -1,109 +1,109 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject, onBeforeMount, ref } from 'vue'; import { inject, onBeforeMount, ref } from "vue";
import { import {
Responses, Responses,
check_token, check_token,
TokenInjectType, TokenInjectType,
Admin, Admin,
isErrorResponse isErrorResponse,
} from '@/api'; } from "@/api";
import { onBeforeRouteUpdate } from 'vue-router'; import { onBeforeRouteUpdate } from "vue-router";
import router from '@/router'; import router from "@/router";
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const users = ref<Responses.Admin.GetUsersEntry[]>([]); const users = ref<Responses.Admin.GetUsersEntry[]>([]);
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await updatePanel(); await updatePanel();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await updatePanel(); await updatePanel();
}); });
async function updatePanel() { async function updatePanel() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.get_users(token); const res = await Admin.get_users(token);
if (isErrorResponse(res)) return router.replace({ path: '/' }); if (isErrorResponse(res)) return router.replace({ path: "/" });
users.value = res.users; users.value = res.users;
} }
async function setRole(user: number, roleStr: string) { async function setRole(user: number, roleStr: string) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.set_role(user, parseInt(roleStr, 10), token); const res = await Admin.set_role(user, parseInt(roleStr, 10), token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function disableTfa(user: number) { async function disableTfa(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.disable_tfa(user, token); const res = await Admin.disable_tfa(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function logoutUser(user: number) { async function logoutUser(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.logout(user, token); const res = await Admin.logout(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
async function deleteUser(user: number) { async function deleteUser(user: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Admin.delete_user(user, token); const res = await Admin.delete_user(user, token);
if (isErrorResponse(res)) console.error(res.message); if (isErrorResponse(res)) console.error(res.message);
await updatePanel(); await updatePanel();
} }
</script> </script>
<template> <template>
<table> <table>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Type</th> <th>Type</th>
<th>Role</th> <th>Role</th>
<th>Tfa Status</th> <th>Tfa Status</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
<tr v-for="user in users" :key="user.id"> <tr v-for="user in users" :key="user.id">
<td>{{ user.name }}</td> <td>{{ user.name }}</td>
<td>{{ user.gitlab ? 'Gitlab' : 'Password' }}</td> <td>{{ user.gitlab ? "Gitlab" : "Password" }}</td>
<td> <td>
<select @change="setRole(user.id, $event.target.value)"> <select @change="setRole(user.id, $event.target.value)">
<option value="0" :selected="user.role === 0 ? true : null"> <option value="0" :selected="user.role === 0 ? true : null">
Disabled Disabled
</option> </option>
<option value="1" :selected="user.role === 1 ? true : null"> <option value="1" :selected="user.role === 1 ? true : null">
User User
</option> </option>
<option value="2" :selected="user.role === 2 ? true : null"> <option value="2" :selected="user.role === 2 ? true : null">
Admin Admin
</option> </option>
</select> </select>
</td> </td>
<td v-if="user.gitlab"></td> <td v-if="user.gitlab"></td>
<td v-else> <td v-else>
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }} {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
</td> </td>
<td> <td>
<button v-if="user.tfaEnabled" @click="disableTfa(user.id)"> <button v-if="user.tfaEnabled" @click="disableTfa(user.id)">
Disable Tfa Disable Tfa
</button> </button>
<button @click="logoutUser(user.id)">Logout all</button> <button @click="logoutUser(user.id)">Logout all</button>
<button @click="deleteUser(user.id)">Delete</button> <button @click="deleteUser(user.id)">Delete</button>
</td> </td>
</tr> </tr>
</table> </table>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,70 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'; import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
import { inject, onBeforeMount, ref } from 'vue'; import { inject, onBeforeMount, ref } from "vue";
import { import {
check_token, check_token,
FS, FS,
Responses, Responses,
isErrorResponse, isErrorResponse,
TokenInjectType TokenInjectType,
} from '@/api'; } from "@/api";
import DirViewer from '@/components/FSView/DirViewer.vue'; import DirViewer from "@/components/FSView/DirViewer.vue";
import FileViewer from '@/components/FSView/FileViewer.vue'; import FileViewer from "@/components/FSView/FileViewer.vue";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
const path = ref(''); const path = ref("");
const node = ref<Responses.FS.GetNodeResponse | null>(null); const node = ref<Responses.FS.GetNodeResponse | null>(null);
async function fetch_node(node_id: number) { async function fetch_node(node_id: number) {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
let [p, n] = [ let [p, n] = [
await FS.get_path(token, node_id), await FS.get_path(token, node_id),
await FS.get_node(token, node_id) await FS.get_node(token, node_id),
]; ];
if (isErrorResponse(p)) return gotoRoot(); if (isErrorResponse(p)) return gotoRoot();
if (isErrorResponse(n)) return gotoRoot(); if (isErrorResponse(n)) return gotoRoot();
[path.value, node.value] = [p.path, n]; [path.value, node.value] = [p.path, n];
} }
onBeforeRouteUpdate(async (to) => { onBeforeRouteUpdate(async (to) => {
await fetch_node(Number(to.params.node_id)); await fetch_node(Number(to.params.node_id));
}); });
async function reloadNode() { async function reloadNode() {
await fetch_node(Number(route.params.node_id)); await fetch_node(Number(route.params.node_id));
} }
onBeforeMount(async () => { onBeforeMount(async () => {
await reloadNode(); await reloadNode();
}); });
async function gotoRoot() { async function gotoRoot() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const rootRes = await FS.get_root(token); const rootRes = await FS.get_root(token);
if (isErrorResponse(rootRes)) return jwt.logout(); if (isErrorResponse(rootRes)) return jwt.logout();
const root = rootRes.rootId; const root = rootRes.rootId;
await router.replace({ await router.replace({
name: 'fs', name: "fs",
params: { node_id: root } params: { node_id: root },
}); });
} }
</script> </script>
<template> <template>
<div v-if="node"> <div v-if="node">
<div>Path: {{ path }}</div> <div>Path: {{ path }}</div>
<DirViewer <DirViewer
v-if="!node.isFile" v-if="!node.isFile"
:node="node" :node="node"
@reloadNode="reloadNode" @reloadNode="reloadNode"
@gotoRoot="gotoRoot" @gotoRoot="gotoRoot"
/> />
<FileViewer v-else :node="node" /> <FileViewer v-else :node="node" />
</div> </div>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,28 +1,28 @@
<template><p></p></template> <template><p></p></template>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeRouteUpdate, useRouter } from 'vue-router'; import { onBeforeRouteUpdate, useRouter } from "vue-router";
import { inject, onBeforeMount } from 'vue'; import { inject, onBeforeMount } from "vue";
import { FS, check_token, isErrorResponse, TokenInjectType } from '@/api'; import { FS, check_token, isErrorResponse, TokenInjectType } from "@/api";
const router = useRouter(); const router = useRouter();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function start_redirect() { async function start_redirect() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const root = await FS.get_root(token); const root = await FS.get_root(token);
if (isErrorResponse(root)) return jwt.logout(); if (isErrorResponse(root)) return jwt.logout();
await router.replace({ await router.replace({
name: 'fs', name: "fs",
params: { node_id: root.rootId } params: { node_id: root.rootId },
}); });
} }
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await start_redirect(); await start_redirect();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await start_redirect(); await start_redirect();
}); });
</script> </script>

View File

@ -1,61 +1,61 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue'; import { ref, inject } from "vue";
import { Auth, FS, isErrorResponse, TokenInjectType } from '@/api'; import { Auth, FS, isErrorResponse, TokenInjectType } from "@/api";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const username = ref(''); const username = ref("");
const password = ref(''); const password = ref("");
const otp = ref(''); const otp = ref("");
const error = ref(''); const error = ref("");
const requestOtp = ref(false); const requestOtp = ref(false);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function login() { async function login() {
error.value = ''; error.value = "";
if (username.value === '' || password.value === '') { if (username.value === "" || password.value === "") {
error.value = 'Email and/or Password missing'; error.value = "Email and/or Password missing";
return; return;
} }
const res = await (requestOtp.value const res = await (requestOtp.value
? Auth.auth_login(username.value, password.value, otp.value) ? Auth.auth_login(username.value, password.value, otp.value)
: Auth.auth_login(username.value, password.value)); : Auth.auth_login(username.value, password.value));
if (isErrorResponse(res)) error.value = 'Login failed: ' + res.message; if (isErrorResponse(res)) error.value = "Login failed: " + res.message;
else if ('jwt' in res) { else if ("jwt" in res) {
const root = await FS.get_root(res.jwt); const root = await FS.get_root(res.jwt);
if (isErrorResponse(root)) { if (isErrorResponse(root)) {
error.value = 'Get root failed: ' + root.message; error.value = "Get root failed: " + root.message;
return; return;
} }
jwt.setToken(res.jwt); jwt.setToken(res.jwt);
await router.push({ await router.push({
name: 'fs', name: "fs",
params: { node_id: root.rootId } params: { node_id: root.rootId },
}); });
} else { } else {
error.value = ''; error.value = "";
requestOtp.value = true; requestOtp.value = true;
} }
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<template v-if="!requestOtp"> <template v-if="!requestOtp">
<input type="email" placeholder="Email" v-model="username" /> <input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" /> <input type="password" placeholder="Password" v-model="password" />
<a href="/api/auth/gitlab">Login with gitlab</a> <a href="/api/auth/gitlab">Login with gitlab</a>
<router-link to="signup">Signup instead?</router-link> <router-link to="signup">Signup instead?</router-link>
</template> </template>
<template v-else> <template v-else>
<div>Please input your 2 factor authentication code</div> <div>Please input your 2 factor authentication code</div>
<input type="text" placeholder="Code" v-model="otp" /> <input type="text" placeholder="Code" v-model="otp" />
</template> </template>
<button @click="login()">Login</button> <button @click="login()">Login</button>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,124 +1,112 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, onBeforeMount } from 'vue'; import { ref, inject, onBeforeMount } from "vue";
import { import {
Auth, Auth,
User, User,
check_token, check_token,
isErrorResponse, isErrorResponse,
TokenInjectType, TokenInjectType,
Responses Responses,
} from '@/api'; } from "@/api";
import { onBeforeRouteUpdate } from 'vue-router'; import { onBeforeRouteUpdate } from "vue-router";
const error = ref(''); const error = ref("");
const oldPw = ref(''); const oldPw = ref("");
const newPw = ref(''); const newPw = ref("");
const newPw2 = ref(''); const newPw2 = ref("");
const user = ref<Responses.User.UserInfoResponse | null>(null); const user = ref<Responses.User.UserInfoResponse | null>(null);
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
onBeforeRouteUpdate(async () => { onBeforeRouteUpdate(async () => {
await updateProfile(); await updateProfile();
}); });
onBeforeMount(async () => { onBeforeMount(async () => {
await updateProfile(); await updateProfile();
}); });
async function updateProfile() { async function updateProfile() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await User.get_user_info(token); const res = await User.get_user_info(token);
if (isErrorResponse(res)) return jwt.logout(); if (isErrorResponse(res)) return jwt.logout();
user.value = res; user.value = res;
} }
async function deleteUser() { async function deleteUser() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await User.delete_user(token); await User.delete_user(token);
jwt.logout(); jwt.logout();
} }
async function logoutAll() { async function logoutAll() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Auth.logout_all(token); await Auth.logout_all(token);
jwt.logout(); jwt.logout();
} }
async function changePw() { async function changePw() {
if (oldPw.value === '' || newPw.value === '' || newPw2.value === '') { if (oldPw.value === "" || newPw.value === "" || newPw2.value === "") {
error.value = 'Password missing'; error.value = "Password missing";
return; return;
} }
if (newPw.value !== newPw2.value) { if (newPw.value !== newPw2.value) {
error.value = "Passwords don't match"; error.value = "Passwords don't match";
return; return;
} }
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
const res = await Auth.change_password(oldPw.value, newPw.value, token); const res = await Auth.change_password(oldPw.value, newPw.value, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Password change failed: ' + res.message; error.value = "Password change failed: " + res.message;
else jwt.logout(); else jwt.logout();
} }
async function tfaDisable() { async function tfaDisable() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
await Auth.tfa_disable(token); await Auth.tfa_disable(token);
jwt.logout(); jwt.logout();
} }
</script> </script>
<template> <template>
<template v-if="user"> <template v-if="user">
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<div>User: {{ user.name }}</div> <div>User: {{ user.name }}</div>
<div>Signed in with {{ user.gitlab ? 'gitlab' : 'password' }}</div> <div>Signed in with {{ user.gitlab ? "gitlab" : "password" }}</div>
<template v-if="!user.gitlab"> <template v-if="!user.gitlab">
<div> <div>
<input <input type="password" placeholder="Old password" v-model="oldPw" />
type="password" <input type="password" placeholder="New password" v-model="newPw" />
placeholder="Old password" <input
v-model="oldPw" type="password"
/> placeholder="Repeat new password"
<input v-model="newPw2"
type="password" />
placeholder="New password" <button @click="changePw">Change</button>
v-model="newPw" </div>
/> <div>
<input <div>
type="password" 2 Factor authentication:
placeholder="Repeat new password" {{ user.tfaEnabled ? "Enabled" : "Disabled" }}
v-model="newPw2" </div>
/> <div>
<button @click="changePw">Change</button> <a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> Disable </a>
</div> <router-link to="/profile/2fa-enable" v-else> Enable </router-link>
<div> </div>
<div> </div>
2 Factor authentication: </template>
{{ user.tfaEnabled ? 'Enabled' : 'Disabled' }} <div>
</div> <a href="#" @click="logoutAll">Logout everywhere</a>
<div> <a href="#" @click="deleteUser">Delete Account</a>
<a href="#" v-if="user.tfaEnabled" @click="tfaDisable"> </div>
Disable </template>
</a> <template v-else>
<router-link to="/profile/2fa-enable" v-else> <div>Loading...</div>
Enable </template>
</router-link>
</div>
</div>
</template>
<div>
<a href="#" @click="logoutAll">Logout everywhere</a>
<a href="#" @click="deleteUser">Delete Account</a>
</div>
</template>
<template v-else>
<div>Loading...</div>
</template>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,19 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { inject } from 'vue'; import { inject } from "vue";
import { TokenInjectType } from '@/api'; import { TokenInjectType } from "@/api";
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
if ('token' in route.query) jwt.setToken(route.query['token'] as string); if ("token" in route.query) jwt.setToken(route.query["token"] as string);
router.replace({ path: '/' }); router.replace({ path: "/" });
</script> </script>
<template> <template>
<router-link to="/">Click here to go home</router-link> <router-link to="/">Click here to go home</router-link>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,35 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from "vue";
import { Auth, isErrorResponse } from '@/api'; import { Auth, isErrorResponse } from "@/api";
const username = ref(''); const username = ref("");
const password = ref(''); const password = ref("");
const password2 = ref(''); const password2 = ref("");
const error = ref(''); const error = ref("");
async function signup() { async function signup() {
if (username.value === '' || password.value === '') { if (username.value === "" || password.value === "") {
error.value = 'Email and/or Password missing'; error.value = "Email and/or Password missing";
return; return;
} }
if (password.value !== password2.value) { if (password.value !== password2.value) {
error.value = "Passwords don't match"; error.value = "Passwords don't match";
return; return;
} }
const res = await Auth.auth_signup(username.value, password.value); const res = await Auth.auth_signup(username.value, password.value);
error.value = isErrorResponse(res) error.value = isErrorResponse(res)
? 'Signup failed: ' + res.message ? "Signup failed: " + res.message
: 'Signup successful, please wait till an admin unlocks your account.'; : "Signup successful, please wait till an admin unlocks your account.";
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<input type="email" placeholder="Email" v-model="username" /> <input type="email" placeholder="Email" v-model="username" />
<input type="password" placeholder="Password" v-model="password" /> <input type="password" placeholder="Password" v-model="password" />
<input type="password" placeholder="Repeat password" v-model="password2" /> <input type="password" placeholder="Repeat password" v-model="password2" />
<button @click="signup()">Signup</button> <button @click="signup()">Signup</button>
<router-link to="login">Login instead?</router-link> <router-link to="login">Login instead?</router-link>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,89 +1,89 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject } from 'vue'; import { ref, inject } from "vue";
import { Auth, check_token, isErrorResponse, TokenInjectType } from '@/api'; import { Auth, check_token, isErrorResponse, TokenInjectType } from "@/api";
enum state { enum state {
SELECT, SELECT,
MAIL, MAIL,
TOTP TOTP,
} }
const currentState = ref<state>(state.SELECT); const currentState = ref<state>(state.SELECT);
const error = ref(''); const error = ref("");
const qrImage = ref(''); const qrImage = ref("");
const secret = ref(''); const secret = ref("");
const code = ref(''); const code = ref("");
const jwt = inject<TokenInjectType>('jwt') as TokenInjectType; const jwt = inject<TokenInjectType>("jwt") as TokenInjectType;
async function selectMail() { async function selectMail() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_setup(true, token); const res = await Auth.tfa_setup(true, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message; error.value = "Failed to select 2fa type: " + res.message;
else { else {
error.value = ''; error.value = "";
currentState.value = state.MAIL; currentState.value = state.MAIL;
} }
} }
async function selectTotp() { async function selectTotp() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_setup(false, token); const res = await Auth.tfa_setup(false, token);
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to select 2fa type: ' + res.message; error.value = "Failed to select 2fa type: " + res.message;
else { else {
qrImage.value = res.qrCode; qrImage.value = res.qrCode;
secret.value = res.secret; secret.value = res.secret;
error.value = ''; error.value = "";
currentState.value = state.TOTP; currentState.value = state.TOTP;
} }
} }
async function submit() { async function submit() {
const token = await check_token(jwt); const token = await check_token(jwt);
if (!token) return; if (!token) return;
error.value = 'Working...'; error.value = "Working...";
const res = await Auth.tfa_complete( const res = await Auth.tfa_complete(
currentState.value === state.MAIL, currentState.value === state.MAIL,
code.value, code.value,
token token
); );
if (isErrorResponse(res)) if (isErrorResponse(res))
error.value = 'Failed to submit code: ' + res.message; error.value = "Failed to submit code: " + res.message;
else jwt.logout(); else jwt.logout();
} }
</script> </script>
<template> <template>
<div v-if="error !== ''" v-text="error"></div> <div v-if="error !== ''" v-text="error"></div>
<template v-if="currentState === state.SELECT"> <template v-if="currentState === state.SELECT">
<div>Select 2 Factor authentication type:</div> <div>Select 2 Factor authentication type:</div>
<div> <div>
<button @click="selectMail">Mail</button> <button @click="selectMail">Mail</button>
<button @click="selectTotp">Google Authenticator</button> <button @click="selectTotp">Google Authenticator</button>
</div> </div>
</template> </template>
<template v-else-if="currentState === state.MAIL"> <template v-else-if="currentState === state.MAIL">
<div>Please enter the code you got by mail</div> <div>Please enter the code you got by mail</div>
<input type="text" placeholder="Code" v-model="code" /> <input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button> <button @click="submit()">Submit</button>
</template> </template>
<template v-else> <template v-else>
<img :src="qrImage" alt="QrCode" /> <img :src="qrImage" alt="QrCode" />
<details> <details>
<summary>Show manual input code</summary> <summary>Show manual input code</summary>
{{ secret }} {{ secret }}
</details> </details>
<div>Please enter the current code</div> <div>Please enter the current code</div>
<input type="text" placeholder="Code" v-model="code" /> <input type="text" placeholder="Code" v-model="code" />
<button @click="submit()">Submit</button> <button @click="submit()">Submit</button>
</template> </template>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@ -1,12 +1,12 @@
const { defineConfig } = require('@vue/cli-service'); const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
configureWebpack: { configureWebpack: {
resolve: { resolve: {
fallback: { fallback: {
crypto: false, crypto: false,
stream: require.resolve('stream-browserify') stream: require.resolve("stream-browserify"),
} },
} },
} },
}); });

View File

@ -1,9 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"monorepo": true,
"sourceRoot": "src",
"compilerOptions": {
"tsConfigPath": "tsconfig.json"
}
}

View File

@ -1,122 +0,0 @@
{
"name": "fileserver",
"private": true,
"version": "1.0.0",
"description": "Crackhead fileserver",
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"lint": "eslint \"src/**/*.ts\"",
"lint-fix": "eslint \"src/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"genapi": "ts-node tools/apigen.ts",
"updateDto": "cd dto && yarn build && cd .. && yarn add ./dto && cd frontend && yarn add ../dto",
"lint-fix-all": "yarn lint-fix && cd dto && yarn lint-fix && cd ../frontend && yarn lint --fix"
},
"dependencies": {
"@fastify/multipart": "^7.1.0",
"@fastify/static": "^6.5.0",
"@nestjs/common": "^9.0.8",
"@nestjs/core": "^9.0.8",
"@nestjs/jwt": "^9.0.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-fastify": "^9.0.8",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/typeorm": "^9.0.0",
"argon2": "^0.28.7",
"axios": "^0.27.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.7.8",
"notp": "^2.0.3",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"qrcode": "^1.5.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.6",
"sqlite3": "^5.0.11",
"thirty-two": "^1.0.2",
"typeorm": "^0.3.7"
},
"runtimeDependencies": [
"@fastify/multipart",
"@fastify/static",
"@nestjs/common",
"@nestjs/core",
"@nestjs/platform-fastify",
"@nestjs/serve-static",
"argon2",
"class-transformer",
"class-validator",
"reflect-metadata",
"rxjs",
"sqlite3",
"typeorm"
],
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.8",
"@types/express": "^4.17.13",
"@types/jest": "^28.1.6",
"@types/jsonwebtoken": "^8.5.8",
"@types/node": "^18.6.5",
"@types/nodemailer": "^6.4.5",
"@types/notp": "^2.0.2",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"@types/qrcode": "^1.5.0",
"@types/supertest": "^2.0.12",
"@types/webpack": "^5.28.0",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"@typescript-eslint/typescript-estree": "^5.33.0",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-no-relative-import-paths": "^1.4.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^28.1.3",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"source-map-support": "^0.5.21",
"supertest": "^6.2.4",
"ts-jest": "^28.0.7",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-node-externals": "^3.0.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1,28 +0,0 @@
### Create account
POST http://127.0.0.1:8080/api/auth/signup
Content-Type: application/json
{"username": "root@mattv.de", "password": "123"}
### Wrong authenctication
POST http://127.0.0.1:8080/api/auth/login
Content-Type: application/json
{"username": "root@mattv.de", "password": "this is not correct"}
### Correct authentication
POST http://127.0.0.1:8080/api/auth/login
Content-Type: application/json
{"username": "root@mattv.de", "password": "123"}
> {% client.global.set("auth_token", response.body.jwt); %}
### Check if authenticated with admin perms
GET http://127.0.0.1:8080/test/hello2
Authorization: Bearer {{auth_token}}
### Refresh token
POST http://127.0.0.1:8080/api/auth/refresh
Authorization: Bearer {{auth_token}}

View File

@ -1,59 +0,0 @@
import { Controller, Get, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { INode, JWTToken, User } from './entities';
import FileSystemModule from './modules/filesystem';
import { JWTAuthGuard, Role, RoleGuard } from './authguards';
import AuthModule from './modules/auth';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { cwd } from 'process';
import { UserRole } from '../dto/';
declare const PROD: boolean | undefined;
@Controller('test')
class TestController {
@Role(UserRole.USER)
@Get('hello')
getHello(): string {
return 'UwU';
}
@Role(UserRole.ADMIN)
@Get('hello2')
getHelloAdmin(): string {
return 'UwU Admin';
}
}
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'sqlite.db',
synchronize: true,
entities: [User, INode, JWTToken]
}),
ServeStaticModule.forRoot({
rootPath:
typeof PROD !== 'undefined' && PROD
? join(cwd(), 'frontend')
: join(__dirname, '..', '..', 'frontend', 'dist'),
exclude: ['/api*']
}),
FileSystemModule,
AuthModule
],
controllers: [TestController],
providers: [
{
provide: 'APP_GUARD',
useClass: JWTAuthGuard
},
{
provide: 'APP_GUARD',
useClass: RoleGuard
}
]
})
export class AppModule {}

View File

@ -1,47 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
SetMetadata
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { User } from './entities';
import { UserRole } from '../dto';
const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Injectable()
export class JWTAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()]
);
if (isPublic) return true;
return super.canActivate(context);
}
}
const ROLE_KEY = 'role';
export const Role = (role: UserRole) => SetMetadata(ROLE_KEY, role);
@Injectable()
export class RoleGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext) {
const requiredRole = this.reflector.getAllAndOverride<UserRole>(
ROLE_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredRole) return true;
const user: User = context.switchToHttp().getRequest().user;
return user.role >= requiredRole;
}
}

View File

@ -1,83 +0,0 @@
import {
BadRequestException,
Body,
Controller,
Get,
Post,
Request,
ValidationPipe
} from '@nestjs/common';
import { AuthService } from 'services/auth';
import { Requests, Responses, UserRole } from '../../dto';
import { Role } from 'authguards';
import { tfaTypes } from 'entities';
@Controller('api/admin')
export default class AdminController {
constructor(private authService: AuthService) {}
@Role(UserRole.ADMIN)
@Get('users')
async getUsers(): Promise<Responses.Admin.GetUsers> {
const users = await this.authService.getUsers();
const entries = users.map(
(user) =>
new Responses.Admin.GetUsersEntry(
user.id,
user.isGitlabUser,
user.name,
user.role,
this.authService.requiresTfa(user)
)
);
return new Responses.Admin.GetUsers(entries);
}
@Role(UserRole.ADMIN)
@Post('set_role')
async setRole(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Admin.SetUserRole
): Promise<Responses.Admin.SetUserRole> {
const user = await this.authService.getUser(data.user);
if (!user) throw new BadRequestException('Invalid user');
await this.authService.setUserRole(user, data.role);
return new Responses.Admin.SetUserRole();
}
@Role(UserRole.ADMIN)
@Post('logout')
async logout(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Admin.LogoutAll
): Promise<Responses.Admin.LogoutAllUser> {
const user = await this.authService.getUser(data.user);
if (!user) throw new BadRequestException('Invalid user');
await this.authService.revokeAll(user);
return new Responses.Admin.LogoutAllUser();
}
@Role(UserRole.ADMIN)
@Post('delete')
async delete(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Admin.DeleteUser
): Promise<Responses.Admin.DeleteUser> {
const user = await this.authService.getUser(data.user);
if (!user) throw new BadRequestException('Invalid user');
await this.authService.deleteUser(user);
return new Responses.Admin.DeleteUser();
}
@Role(UserRole.ADMIN)
@Post('disable_2fa')
async disableTfa(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Admin.DisableTfa
): Promise<Responses.Admin.DisableTfa> {
const user = await this.authService.getUser(data.user);
if (!user) throw new BadRequestException('Invalid user');
await this.authService.setTfaType(user, tfaTypes.NONE);
return new Responses.Admin.DisableTfa();
}
}

View File

@ -1,150 +0,0 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
Post,
Query,
Redirect,
Request,
UnauthorizedException,
UseGuards,
ValidationPipe
} from '@nestjs/common';
import { AuthService } from 'services/auth';
import { AuthGuard } from '@nestjs/passport';
import { Public } from 'authguards';
import { Requests, Responses } from '../../dto';
import { tfaTypes } from 'entities';
import { toDataURL } from 'qrcode';
import * as base32 from 'thirty-two';
@Controller('api/auth')
export default class AuthController {
constructor(private authService: AuthService) {}
@Public()
@UseGuards(AuthGuard('local'))
@Post('login')
@HttpCode(200)
async login(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Auth.LoginRequest
): Promise<
Responses.Auth.LoginResponse | Responses.Auth.TfaRequiredResponse
> {
if (this.authService.requiresTfa(req.user)) {
if (!data.otp) {
if (req.user.tfaType == tfaTypes.EMAIL)
await this.authService.sendTfaMail(req.user);
return new Responses.Auth.TfaRequiredResponse();
}
if (!(await this.authService.verifyTfa(req.user, data.otp)))
throw new UnauthorizedException('Incorrect 2fa');
}
return new Responses.Auth.LoginResponse(
await this.authService.login(req, req.user)
);
}
@Post('2fa/disable')
async tfaDisable(
@Request() req
): Promise<Responses.Auth.RemoveTfaResponse> {
await this.authService.setTfaType(req.user, tfaTypes.NONE);
await this.authService.revokeAll(req.user);
return new Responses.Auth.RemoveTfaResponse();
}
@Post('2fa/complete')
async tfaMail(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Auth.TfaComplete
): Promise<Responses.Auth.TfaCompletedResponse> {
const type = data.mail ? tfaTypes.EMAIL : tfaTypes.TOTP;
if (!(await this.authService.verifyTfa(req.user, data.code, type))) {
throw new UnauthorizedException('Incorrect 2fa');
}
await this.authService.setTfaType(req.user, type);
await this.authService.revokeAll(req.user);
return new Responses.Auth.TfaCompletedResponse();
}
@Post('2fa/setup')
async setupTotp(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Auth.TfaSetup
): Promise<
| Responses.Auth.RequestTotpTfaResponse
| Responses.Auth.RequestEmailTfaResponse
> {
const secret = await this.authService.setupTfa(req.user);
if (data.mail) return new Responses.Auth.RequestEmailTfaResponse();
return new Responses.Auth.RequestTotpTfaResponse(
await toDataURL(
`otpauth://totp/MFileserver:${req.user.name}?secret=${base32
.encode(secret)
.toString()}&issuer=MFileserver`
),
base32.encode(secret).toString()
);
}
@Public()
@Post('signup')
async signup(
@Body(new ValidationPipe()) data: Requests.Auth.SignUpRequest
): Promise<Responses.Auth.SignupResponse> {
if ((await this.authService.findUser(data.username, false)) != null)
throw new BadRequestException('Username already taken');
await this.authService.signup(data.username, data.password);
return new Responses.Auth.SignupResponse();
}
@Post('refresh')
async refresh(@Request() req): Promise<Responses.Auth.RefreshResponse> {
const token = await this.authService.login(req, req.user);
await this.authService.revoke(req.token);
return await new Responses.Auth.RefreshResponse(token);
}
@Public()
@Redirect()
@Get('gitlab')
async gitlab(@Request() req) {
return {
url: this.authService.getGitlabAuthUrl(req)
};
}
@Public()
@Redirect()
@Get('gitlab_callback')
async gitlabCallback(@Request() req, @Query('code') code) {
const user = await this.authService.getGitlabUserFromCode(req, code);
const token = await this.authService.login(req, user);
return {
url: `/set_token?token=${token}`
};
}
@Post('change_password')
async changePassword(
@Request() req,
@Body(new ValidationPipe()) data: Requests.Auth.ChangePasswordRequest
): Promise<Responses.Auth.ChangePasswordResponse> {
await this.authService.changePassword(
req.user,
data.oldPassword,
data.newPassword
);
return new Responses.Auth.ChangePasswordResponse();
}
@Post('logout_all')
async logoutAll(@Request() req): Promise<Responses.Auth.LogoutAllResponse> {
await this.authService.revokeAll(req.user);
return new Responses.Auth.LogoutAllResponse();
}
}

View File

@ -1,121 +0,0 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
Request,
StreamableFile,
ValidationPipe
} from '@nestjs/common';
import { Responses, Requests, validateAsyncInline, UserRole } from '../../dto';
import FileSystemService from 'services/filesystem';
import { Role } from 'authguards';
@Controller('api/fs')
export default class FileSystemController {
constructor(private fsService: FileSystemService) {}
@Get('root')
@Role(UserRole.USER)
async getRoot(@Request() req): Promise<Responses.FS.GetRootResponse> {
return new Responses.FS.GetRootResponse(req.user.rootId);
}
@Get('node/:node')
@Role(UserRole.USER)
async getNode(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<Responses.FS.GetNodeResponse> {
const node = await this.fsService.getNodeAndValidate(nodeId, req.user);
const data = new Responses.FS.GetNodeResponse(
nodeId,
node.name,
node.isFile,
node.parentId
);
if (data.isFile) {
data.size = node.size;
} else {
data.children = (await node.children).map((child) => child.id);
}
return validateAsyncInline(data);
}
@Get('path/:node')
@Role(UserRole.USER)
async getPath(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<Responses.FS.GetPathResponse> {
return new Responses.FS.GetPathResponse(
await this.fsService.generatePath(
await this.fsService.getNodeAndValidate(nodeId, req.user)
)
);
}
@Post('createFolder')
@Role(UserRole.USER)
async createFolder(
@Request() req,
@Body(new ValidationPipe()) data: Requests.FS.CreateFolderRequest
): Promise<Responses.FS.CreateFolderResponse> {
const newChild = await this.fsService.create(
await this.fsService.getNodeAndValidate(data.parent, req.user),
data.name,
req.user,
false
);
return new Responses.FS.CreateFolderResponse(newChild.id);
}
@Post('createFile')
@Role(UserRole.USER)
async createFile(
@Request() req,
@Body(new ValidationPipe()) data: Requests.FS.CreateFileRequest
): Promise<Responses.FS.CreateFileResponse> {
const newChild = await this.fsService.create(
await this.fsService.getNodeAndValidate(data.parent, req.user),
data.name,
req.user,
true
);
return new Responses.FS.CreateFileResponse(newChild.id);
}
@Post('delete')
@Role(UserRole.USER)
async delete(
@Request() req,
@Body(new ValidationPipe()) data: Requests.FS.DeleteRequest
): Promise<Responses.FS.DeleteResponse> {
await this.fsService.delete(
await this.fsService.getNodeAndValidate(data.node, req.user)
);
return new Responses.FS.DeleteResponse();
}
@Post('upload/:node')
@Role(UserRole.USER)
async upload(
@Request() req,
@Param('node', ParseIntPipe) nodeId
): Promise<Responses.FS.UploadFileResponse> {
await this.fsService.uploadFile(await req.file(), nodeId, req.user);
return new Responses.FS.UploadFileResponse();
}
@Post('download')
@Role(UserRole.USER)
async download(
@Request() req,
@Body('id', ParseIntPipe) id
): Promise<StreamableFile> {
return this.fsService.downloadFile(id, req.user);
}
}

View File

@ -1,27 +0,0 @@
import { Controller, Get, Post, Request } from '@nestjs/common';
import { AuthService } from 'services/auth';
import { Responses } from '../../dto';
@Controller('api/user')
export default class UserController {
constructor(private authService: AuthService) {}
@Get('info')
async getUserInfo(
@Request() req
): Promise<Responses.User.UserInfoResponse> {
return new Responses.User.UserInfoResponse(
req.user.name,
req.user.isGitlabUser,
this.authService.requiresTfa(req.user)
);
}
@Post('delete')
async deleteUser(
@Request() req
): Promise<Responses.User.DeleteUserResponse> {
await this.authService.deleteUser(req.user);
return new Responses.User.DeleteUserResponse();
}
}

View File

@ -1,95 +0,0 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
ManyToOne,
OneToMany,
OneToOne
} from 'typeorm';
import { UserRole } from '../dto';
export enum tfaTypes {
NONE = 0,
EMAIL = 1,
TOTP = 2
}
@Entity()
export class INode {
@PrimaryGeneratedColumn()
id: number;
@Column()
isFile: boolean;
@Column()
name: string;
@Column({ nullable: true })
size: number;
@Column({ nullable: true })
parentId: number;
@ManyToOne(() => INode, (node) => node.children)
parent: Promise<INode>;
@OneToMany(() => INode, (node) => node.parent)
children: Promise<INode[]>;
@Column({ nullable: true })
ownerId: number;
@ManyToOne(() => User)
owner: Promise<User>;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ default: false })
isGitlabUser: boolean;
@Column()
name: string;
@Column()
password: string;
@Column({
type: 'int',
default: UserRole.DISABLED,
transformer: {
from: (db: number): UserRole => db,
to: (role: UserRole): number => role
}
})
role: UserRole;
@Column({ nullable: true })
rootId: number;
@OneToOne(() => INode)
root: Promise<INode>;
@Column({
type: 'int',
default: tfaTypes.NONE,
transformer: {
from: (db: number): tfaTypes => db,
to: (type: tfaTypes): number => type
}
})
tfaType: tfaTypes;
@Column({ nullable: true })
tfaSecret: string;
@Column({ nullable: true })
gitlabAT: string;
@Column({ nullable: true })
gitlabRT: string;
}
@Entity()
export class JWTToken {
@PrimaryGeneratedColumn()
id: number;
@Column()
ownerId: number;
@Column({ nullable: true })
exp: number;
}

View File

@ -1,20 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
FastifyAdapter,
NestFastifyApplication
} from '@nestjs/platform-fastify';
import fastifyMultipart from '@fastify/multipart';
import { existsSync, mkdirSync } from 'fs';
async function bootstrap() {
if (!existsSync('files')) mkdirSync('files');
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: true })
);
await app.register(fastifyMultipart);
await app.listen(8080, '0.0.0.0');
}
bootstrap();

Some files were not shown because too many files have changed in this diff Show More