Add project files
This commit is contained in:
26
.dockerignore
Normal file
26
.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
**/db_data
|
||||
LICENSE
|
||||
README.md
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/db_data/
|
||||
4
.idea/.idea.T120B165-ImgBoard/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.T120B165-ImgBoard/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
8
.idea/.idea.T120B165-ImgBoard/.idea/indexLayout.xml
generated
Normal file
8
.idea/.idea.T120B165-ImgBoard/.idea/indexLayout.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/.idea.T120B165-ImgBoard/.idea/projectSettingsUpdater.xml
generated
Normal file
8
.idea/.idea.T120B165-ImgBoard/.idea/projectSettingsUpdater.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="singleClickDiffPreview" value="1" />
|
||||
<option name="unhandledExceptionsIgnoreList" value="1" />
|
||||
<option name="vcsConfiguration" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.T120B165-ImgBoard/.idea/vcs.xml
generated
Normal file
6
.idea/.idea.T120B165-ImgBoard/.idea/vcs.xml
generated
Normal 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>
|
||||
240
.idea/.idea.T120B165-ImgBoard/.idea/workspace.xml
generated
Normal file
240
.idea/.idea.T120B165-ImgBoard/.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,240 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoGeneratedRunConfigurationManager">
|
||||
<projectFile profileName="http">T120B165-ImgBoard/T120B165-ImgBoard.csproj</projectFile>
|
||||
<projectFile profileName="https">T120B165-ImgBoard/T120B165-ImgBoard.csproj</projectFile>
|
||||
</component>
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="4e14f3e6-3f4d-450c-8a9e-273b19823ca0" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.T120B165-ImgBoard/.idea/encodings.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.T120B165-ImgBoard/.idea/indexLayout.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.T120B165-ImgBoard/.idea/projectSettingsUpdater.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.T120B165-ImgBoard/.idea/vcs.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.T120B165-ImgBoard/.idea/workspace.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard.sln" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard.sln.DotSettings.user" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/.gitignore" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Controllers/AuthController.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Controllers/PostController.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Controllers/TagController.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Data/DbInitializer.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Data/ImgBoardContext.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dockerfile" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/Comment/CommentDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/LoginDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/Post/CreatePostDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/Post/PostDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/RefreshDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/RegisterDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/SlimUserDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/Tag/CreateTagDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/TokenDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Dtos/UserDto.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Models/Comment.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Models/File.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Models/Post.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Models/Tag.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Models/User.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Program.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Properties/launchSettings.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/CommentService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/FileService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/PostService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/TagService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/TokenService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Services/UserService.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/T120B165-ImgBoard.csproj" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/Utils/PagedList.cs" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/T120B165-ImgBoard/appsettings.Development.json" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/compose.yaml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="DpaMonitoringSettings">
|
||||
<option name="firstShow" value="false" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/ILViewer/286e202dd8b6498aaf4d306beb6010b86a800/69/ec005a21/02000048.il" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/ILViewer/286e202dd8b6498aaf4d306beb6010b86a800/c0/34726a3e/0200002A.il" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/ILViewer/9dc46363eb12453791df46baf16699c41a928/86/01a85f52/0200001Cpdb2.il" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/286e202dd8b6498aaf4d306beb6010b86a800/42/185ed81a/IUserRoleStore`1.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/286e202dd8b6498aaf4d306beb6010b86a800/81/11a2ff3a/UserManager`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/286e202dd8b6498aaf4d306beb6010b86a800/b5/88947990/IPasswordHasher`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/477051138f1f40de9077b7b1cdc55c6215fb0/d0/53d5065a/JwtRegisteredClaimNames.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/4afd6eaa21b44c15b819957c24b8b6071daa00/a4/86d4eee3/StatusCodeResult.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/560c8584411b436bb783d358249acd4fe400/15/6f12b841/IHostEnvironment.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/8cb87e61921c49bcadb380781eccdeb4228600/16/829c32b2/NSwagApplicationBuilderExtensions.cs" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/b13cb16f7e5749269b6922dc6752bd1c291448/5b/5bdfe94c/DatabaseFacade.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/b9c62e8e682440cd84e6081f7672fcd3d1a000/61/073f2693/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/fe0eb928ebdd4a47ae76673fbad3037d10800/30/30f1fe01/IdentityUser`1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/fe0eb928ebdd4a47ae76673fbad3037d10800/9f/a723ee44/IdentityUser.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/10624a394067e79df04da5d3a67dad2f18e6c46e3cc06bac64a918f6540cc/UserStore.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/f9342fb513b7b524925c79d9822cd3ecaf072c50231aa3b469875f4c2cdf/IdentityDbContext.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="KubernetesApiPersistence">{}</component>
|
||||
<component name="KubernetesApiProvider">{
|
||||
"isMigrated": true
|
||||
}</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 3
|
||||
}</component>
|
||||
<component name="ProjectId" id="32iwKzz0G6elTlo2DhCLGO5rXHo" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
".NET Launch Settings Profile.T120B165-ImgBoard: http.executor": "Run",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"git-widget-placeholder": "main",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.T120B165-ImgBoard: http">
|
||||
<configuration name="T120B165-ImgBoard: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/T120B165-ImgBoard/T120B165-ImgBoard.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="http" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="T120B165-ImgBoard: https" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/T120B165-ImgBoard/T120B165-ImgBoard.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value="net9.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="https" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="T120B165-ImgBoard/Dockerfile" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="t120b165-imgboard" />
|
||||
<option name="containerName" value="t120b165-imgboard" />
|
||||
<option name="contextFolderPath" value="$PROJECT_DIR$" />
|
||||
<option name="portBindings">
|
||||
<list>
|
||||
<DockerPortBindingImpl>
|
||||
<option name="containerPort" value="8080" />
|
||||
<option name="hostIp" value="127.0.0.1" />
|
||||
<option name="hostPort" value="8080" />
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="sourceFilePath" value="T120B165-ImgBoard/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="docker-compose.yml" temporary="true">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings />
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
|
||||
<deployment type="dockerfile">
|
||||
<settings />
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="compose.yaml: Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="sourceFilePath" value="compose.yaml" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="4e14f3e6-3f4d-450c-8a9e-273b19823ca0" name="Changes" comment="" />
|
||||
<created>1757916641972</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1757916641972</updated>
|
||||
<workItem from="1757916644799" duration="6218000" />
|
||||
<workItem from="1757954672397" duration="682000" />
|
||||
<workItem from="1758526401446" duration="4339000" />
|
||||
<workItem from="1758727364837" duration="1057000" />
|
||||
<workItem from="1758786058133" duration="5007000" />
|
||||
<workItem from="1758791727481" duration="3280000" />
|
||||
<workItem from="1759132739938" duration="15494000" />
|
||||
<workItem from="1759305506296" duration="7211000" />
|
||||
<workItem from="1759334279107" duration="1064000" />
|
||||
<workItem from="1759446792755" duration="16154000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
<breakpoints>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
|
||||
<option name="timeStamp" value="1" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
|
||||
<option name="timeStamp" value="2" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
|
||||
<option name="timeStamp" value="3" />
|
||||
</breakpoint>
|
||||
</breakpoints>
|
||||
</breakpoint-manager>
|
||||
<pin-to-top-manager>
|
||||
<pinned-members>
|
||||
<PinnedItemInfo parentTag="Type#System.Security.Claims.ClaimsPrincipal" memberName="Claims" />
|
||||
</pinned-members>
|
||||
</pin-to-top-manager>
|
||||
</component>
|
||||
</project>
|
||||
55
README.md
55
README.md
@@ -7,32 +7,73 @@ Veikimo principas - pati platforma bus prieinama per interneto naršyklę ir bus
|
||||
|
||||
Sistema bus kuriama naudojant modernias technologijas, o diegimas bus supaprastintas, siekiant greitesnio diegimo. Naudotojams bus suteikiami skirtingi vaidmenys su atitinkamomis teisėmis, užtikrinant sklandų ir kontroliuojamą turinio valdymą. Naudotojai galės filtruoti nuotraukas pagal joms paskirtas žymas.
|
||||
|
||||
## Funkciniai reikalavimai
|
||||
### Funkciniai reikalavimai
|
||||
|
||||
### Bendrieji reikalavimai
|
||||
#### Bendrieji reikalavimai
|
||||
- Sistema privalo turėti vartotojo sąsają, kuri leistų peržiūrėti, įkelti, ir tvarkyti nuotraukas bei komentarus.
|
||||
- Duomenų bazė turi būti lengvai keičiama dėl pasirinkto _ORM_ (angl. Object-Relational Mapping) sluoksnio.
|
||||
- Serverio ir klientinės dalys turi būti supakuotos į vieną diegimo vienetą (binary), siekiant supaprastinti diegiamosios sistemos procesą.
|
||||
|
||||
### Rolių reikalavimai
|
||||
#### Rolių reikalavimai
|
||||
|
||||
**! Nubraukti reikalavimai yra kol kas nėra įgyvendinti. Taip pat kol kas nėra rolių.**
|
||||
|
||||
**Svečiai**
|
||||
1. Gali peržiūrėti visus įkeltus paveikslus.
|
||||
2. Gali matyti visus komentarus po paveikslais.
|
||||
3. Gali filtruoti paveikslus pagal nustatytas žymas.
|
||||
3. ~~Gali filtruoti paveikslus pagal nustatytas žymas.~~
|
||||
4. Gali užsiregistruoti ir tapti registruotais naudotojais.
|
||||
|
||||
**Registruoti naudotojai**
|
||||
1. Gali prisijungti prie sistemos.
|
||||
2. Gali atsijungti nuo sistemos.
|
||||
2. ~~Gali atsijungti nuo sistemos.~~
|
||||
3. Gali įkelti naujus paveikslus.
|
||||
4. Gali redaguoti savo įkeltų paveikslų metaduomenis (žymas, aprašą).
|
||||
5. Gali ištrinti savo įkeltus paveikslus.
|
||||
6. Gali rašyti komentarus po paveikslais.
|
||||
7. Gali redaguoti savo komentarus.
|
||||
8. Gali ištrinti savo komentarus.
|
||||
9. Gali keisti savo paskyros informaciją (slaptažodį, el. paštą).
|
||||
9. ~~Gali keisti savo paskyros informaciją (slaptažodį, el. paštą).~~
|
||||
|
||||
**Moderatoriai**
|
||||
1. Turi teisę šalinti ir redaguoti kitų naudotojų paveikslus bei komentarus.
|
||||
2. Gali kurti, redaguoti ir ištrinti žymas, kurios naudojamos turinio kategorizavimui.
|
||||
2. Gali kurti, redaguoti ir ištrinti žymas, kurios naudojamos turinio kategorizavimui.
|
||||
|
||||
## Konfiguracija
|
||||
|
||||
**apsettings.json**
|
||||
```json
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DbContext": "server=localhost;port=3306;database=database_name;user id=user_name;password=password;"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Jwt": {
|
||||
"Key": "openssl rand -base64 32",
|
||||
"Issuer": "ImgBoard",
|
||||
"Audience": "ImgBoard-Users",
|
||||
"DurationInMinutes": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Paleidimas
|
||||
|
||||
Paleidimui naudojama Docker komanda:
|
||||
|
||||
```sh
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
## API dokumentacija
|
||||
|
||||
API dokumentacija yra pasiekiama naudojant `Development` versiją šia nuoroda:
|
||||
http://localhost:5259/swagger/v1/swagger.json
|
||||
|
||||
A copy of the file is provided in the repository called `swagger.json`. May not be up-to-date.
|
||||
21
T120B165-ImgBoard.sln
Normal file
21
T120B165-ImgBoard.sln
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "T120B165-ImgBoard", "T120B165-ImgBoard\T120B165-ImgBoard.csproj", "{FDD7EF71-09BD-4FB3-B0A4-18AFEE0794B1}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0D67DB4A-BAE6-47D3-8E4E-4D3814C796E5}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
compose.yaml = compose.yaml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FDD7EF71-09BD-4FB3-B0A4-18AFEE0794B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FDD7EF71-09BD-4FB3-B0A4-18AFEE0794B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FDD7EF71-09BD-4FB3-B0A4-18AFEE0794B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FDD7EF71-09BD-4FB3-B0A4-18AFEE0794B1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
11
T120B165-ImgBoard.sln.DotSettings.user
Normal file
11
T120B165-ImgBoard.sln.DotSettings.user
Normal file
@@ -0,0 +1,11 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADatabaseFacade_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb13cb16f7e5749269b6922dc6752bd1c291448_003F5b_003F5bdfe94c_003FDatabaseFacade_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityDbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Ff9342fb513b7b524925c79d9822cd3ecaf072c50231aa3b469875f4c2cdf_003FIdentityDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe0eb928ebdd4a47ae76673fbad3037d10800_003F9f_003Fa723ee44_003FIdentityUser_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIdentityUser_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffe0eb928ebdd4a47ae76673fbad3037d10800_003F30_003F30f1fe01_003FIdentityUser_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIPasswordHasher_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F286e202dd8b6498aaf4d306beb6010b86a800_003Fb5_003F88947990_003FIPasswordHasher_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AJwtRegisteredClaimNames_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F477051138f1f40de9077b7b1cdc55c6215fb0_003Fd0_003F53d5065a_003FJwtRegisteredClaimNames_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStatusCodeResult_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F4afd6eaa21b44c15b819957c24b8b6071daa00_003Fa4_003F86d4eee3_003FStatusCodeResult_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb9c62e8e682440cd84e6081f7672fcd3d1a000_003F61_003F073f2693_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserManager_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F286e202dd8b6498aaf4d306beb6010b86a800_003F81_003F11a2ff3a_003FUserManager_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserStore_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F10624a394067e79df04da5d3a67dad2f18e6c46e3cc06bac64a918f6540cc_003FUserStore_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||
4
T120B165-ImgBoard/.gitignore
vendored
Normal file
4
T120B165-ImgBoard/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/bin/
|
||||
/obj/
|
||||
/Storage/
|
||||
/appsettings.json
|
||||
62
T120B165-ImgBoard/Controllers/AuthController.cs
Normal file
62
T120B165-ImgBoard/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController(UserManager<User> userManager, ITokenService tokenService): ControllerBase
|
||||
{
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<User>> Register(RegisterDto dto)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
UserName = dto.UserName,
|
||||
Email = dto.Email,
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(user, dto.Password);
|
||||
await userManager.AddToRoleAsync(user, UserRoles.Regular);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return BadRequest(result.Errors);
|
||||
}
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<TokenDto>> Login(LoginDto dto)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(dto.Email);
|
||||
if (user == null || !await userManager.CheckPasswordAsync(user, dto.Password))
|
||||
return Unauthorized();
|
||||
|
||||
var accessToken = await tokenService.GenerateJwtToken(user);
|
||||
var refreshToken = await tokenService.GenerateRefreshToken(user);
|
||||
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: refreshToken));
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<TokenDto>> Refresh(RefreshDto dto)
|
||||
{
|
||||
var token = await tokenService.GetRefreshTokenByValue(dto.RefreshToken);
|
||||
if (token == null) return Unauthorized();
|
||||
var user = token.User;
|
||||
|
||||
await tokenService.InvalidateRefreshToken(token);
|
||||
|
||||
var accessToken = await tokenService.GenerateJwtToken(user);
|
||||
var newRefreshToken = await tokenService.GenerateRefreshToken(user);
|
||||
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: newRefreshToken));
|
||||
}
|
||||
}
|
||||
456
T120B165-ImgBoard/Controllers/PostController.cs
Normal file
456
T120B165-ImgBoard/Controllers/PostController.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileSignatures;
|
||||
using FileSignatures.Formats;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos.Comment;
|
||||
using T120B165_ImgBoard.Dtos.Post;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/posts")]
|
||||
public class PostController(
|
||||
IPostService postService,
|
||||
IUserService userService,
|
||||
ITagService tagService,
|
||||
ICommentService commentService,
|
||||
IFileService fileService,
|
||||
IWebHostEnvironment env
|
||||
): ControllerBase
|
||||
{
|
||||
|
||||
private static async Task<string> InspectFileContent(string filePath)
|
||||
{
|
||||
// Define the types we support
|
||||
var inspector = new FileFormatInspector(new List<FileFormat>
|
||||
{
|
||||
new Jpeg(),
|
||||
new Png(),
|
||||
});
|
||||
|
||||
// Open the file and inspect
|
||||
await using var stream = System.IO.File.OpenRead(filePath);
|
||||
var format = inspector.DetermineFileFormat(stream);
|
||||
|
||||
// If the file matches a known format, return its MIME type
|
||||
if (format != null) return format.MediaType;
|
||||
|
||||
// If no signature matches, assume generic binary type
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private string GetFinalFilePath(Models.File fileRecord)
|
||||
{
|
||||
var baseDir = Path.Combine(env.ContentRootPath, "Storage", "Attachments");
|
||||
|
||||
// Create a clean, organized subdirectory (e.g., /Storage/Attachments/2025/10/)
|
||||
var dateDir = Path.Combine(baseDir, DateTime.UtcNow.Year.ToString(), DateTime.UtcNow.Month.ToString("D2"));
|
||||
|
||||
// Ensure the directory exists before attempting to save the file
|
||||
if (!Directory.Exists(dateDir))
|
||||
{
|
||||
Directory.CreateDirectory(dateDir);
|
||||
}
|
||||
|
||||
// Create a unique file name to avoid collisions
|
||||
// Use the file ID or a GUID + a sanitized version of the original file name
|
||||
var uniqueFileName = $"{Guid.NewGuid()}-{Path.GetFileName(fileRecord.OriginalFileName)}";
|
||||
|
||||
return Path.Combine(dateDir, uniqueFileName);
|
||||
}
|
||||
|
||||
private static bool TryParseContentRange(string header, out long start, out long end, out long totalSize)
|
||||
{
|
||||
start = end = totalSize = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
return false;
|
||||
|
||||
// We expect a format like "bytes 0-1048575/5242880"
|
||||
// Regex pattern to capture the parts
|
||||
var match = Regex.Match(header, @"bytes\s+(?<start>\d+)-(?<end>\d+)/(?<total>\d+)");
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
// Safely parse the captured groups
|
||||
if (!long.TryParse(match.Groups["start"].Value, out start) ||
|
||||
!long.TryParse(match.Groups["end"].Value, out end) ||
|
||||
!long.TryParse(match.Groups["total"].Value, out totalSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The length of the chunk must match the difference between end and start
|
||||
// Note: 'end' is inclusive, so the number of bytes is (end - start + 1)
|
||||
return start >= 0 && end >= start && totalSize > 0 && end < totalSize;
|
||||
}
|
||||
|
||||
private async Task<ActionResult<List<Tag>>> TagNamesToTags(List<string> tagNames)
|
||||
{
|
||||
var tags = new List<Tag>();
|
||||
foreach (var tagName in tagNames)
|
||||
{
|
||||
var tag = await tagService.GetByName(tagName);
|
||||
if (tag == null)
|
||||
return Problem(
|
||||
detail: $"'{tagName}' is not a valid tag",
|
||||
statusCode: StatusCodes.Status400BadRequest
|
||||
);
|
||||
tags.Add(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PostDto>> Create(CreatePostDto dto)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var user = await userService.GetUserById(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var maybeTags = await TagNamesToTags(dto.Tags);
|
||||
List<Tag> tags;
|
||||
if (maybeTags.Value != null)
|
||||
{
|
||||
tags = maybeTags.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(maybeTags.Result != null);
|
||||
return maybeTags.Result;
|
||||
}
|
||||
var created = await postService.Create(
|
||||
dto.Title, dto.Description, tags, user,
|
||||
dto.FileName, dto.FileMimeType, dto.FileSize.Value
|
||||
);
|
||||
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = created.Post.Id, fileId = created.File.Id }, Request.Scheme
|
||||
);
|
||||
return CreatedAtAction(nameof(Get), new { id = created.Post.Id },
|
||||
PostDto.FromPost(created.Post, fileUrl));
|
||||
}
|
||||
|
||||
[HttpPatch("{postId:int}/files/{fileId:int}")]
|
||||
public async Task<IActionResult> PatchFileContent(int postId, int fileId)
|
||||
{
|
||||
var post = await postService.GetById(postId, includeUnfinished: true);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var fileRecord = await fileService.GetFileById(fileId);
|
||||
if (fileRecord == null) return NotFound();
|
||||
|
||||
if (fileRecord.FinishedDate != null)
|
||||
return Problem(statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: "File was already uploaded.");
|
||||
|
||||
|
||||
// Parse the Content-Range Header
|
||||
// Example: Content-Range: bytes 0-1048575/5242880
|
||||
var rangeHeader = Request.Headers.ContentRange.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(rangeHeader) || !TryParseContentRange(rangeHeader, out long start, out long end, out long totalSizeFromHeader))
|
||||
{
|
||||
return Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: "Missing or invalid Content-Range header."
|
||||
);
|
||||
}
|
||||
|
||||
var totalSize = fileRecord.Size;
|
||||
if (totalSizeFromHeader != totalSize)
|
||||
{
|
||||
return Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: $"Total file size mismatch. Expected: {totalSize} bytes, Received: {totalSizeFromHeader} bytes."
|
||||
);
|
||||
}
|
||||
|
||||
// Append the chunk to the temporary file
|
||||
var tempFilePath = fileRecord.FilePath;
|
||||
await using (var stream = new FileStream(tempFilePath, FileMode.Append, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
|
||||
{
|
||||
stream.Seek(start, SeekOrigin.Begin);
|
||||
await Request.Body.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// Check if the upload is complete
|
||||
// Return 202 Accepted for a successful intermediate chunk
|
||||
if (end + 1 != totalSize) return Accepted();
|
||||
|
||||
// Otherwise, all chunks received. Finalize the file.
|
||||
var finalFilePath = GetFinalFilePath(fileRecord);
|
||||
System.IO.File.Move(tempFilePath, finalFilePath, overwrite: true);
|
||||
|
||||
// Compare the actual type to the expected type
|
||||
var actualContentType = await InspectFileContent(finalFilePath);
|
||||
if (!string.Equals(actualContentType, fileRecord.ContentType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the file's content doesn't match its declared MIME type, clean up
|
||||
System.IO.File.Delete(finalFilePath);
|
||||
await fileService.Delete(fileRecord);
|
||||
//await postService.Delete(post);
|
||||
|
||||
return Problem(statusCode: StatusCodes.Status415UnsupportedMediaType,
|
||||
detail: $"Uploaded file type '{actualContentType}' does not match declared type '{fileRecord.ContentType}'.");
|
||||
}
|
||||
|
||||
// Update the database record with the final path
|
||||
fileRecord.FilePath = finalFilePath;
|
||||
fileRecord.FinishedDate = DateTime.Now;
|
||||
await fileService.Update(fileRecord);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/files/{fileId:int}")]
|
||||
public async Task<IActionResult> GetFileContent(int postId, int fileId)
|
||||
{
|
||||
var post = await postService.GetById(postId, includeUnfinished: true);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var fileRecord = await fileService.GetFileById(fileId);
|
||||
if (fileRecord == null) return NotFound();
|
||||
|
||||
// Ensure the file has been successfully uploaded and finalized
|
||||
if (string.IsNullOrEmpty(fileRecord.FilePath) || fileRecord.FinishedDate == null)
|
||||
{
|
||||
return Problem(statusCode: StatusCodes.Status404NotFound,
|
||||
detail: "File content is not yet available or was corrupted.");
|
||||
}
|
||||
var fullPath = Path.Combine(env.ContentRootPath, fileRecord.FilePath);
|
||||
|
||||
// Check if the file actually exists
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
{
|
||||
return Problem(statusCode: StatusCodes.Status404NotFound,
|
||||
detail: "The physical file could not be found.");
|
||||
}
|
||||
|
||||
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return File(stream, fileRecord.ContentType, fileRecord.OriginalFileName);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PostDto>> Get(int id)
|
||||
{
|
||||
var entry = await postService.GetById(id);
|
||||
if (entry == null) return NotFound();
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = entry.Id, fileId = entry.File.Id }, Request.Scheme
|
||||
);
|
||||
return Ok(PostDto.FromPost(entry, fileUrl));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedList<PostDto>>> GetAll([Range(1, int.MaxValue)] int pageNumber = 1)
|
||||
{
|
||||
var list = await postService.GetAll(pageNumber);
|
||||
var newItems = list.Items.Select(i =>
|
||||
{
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = i.Id, fileId = i.File.Id }, Request.Scheme
|
||||
);
|
||||
return PostDto.FromPost(i, fileUrl);
|
||||
}).ToList();
|
||||
return Ok(new PagedList<PostDto>(newItems, list.CurrentPage, list.PageSize, list.TotalCount));
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Delete(int id)
|
||||
{
|
||||
var post = await postService.GetById(id);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != post.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
// Clean up the file record first
|
||||
var fullPath = Path.Combine(env.ContentRootPath, post.File.FilePath);
|
||||
if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
|
||||
await fileService.Delete(post.File);
|
||||
|
||||
var deleted = await postService.Delete(post);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPatch("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PostDto>> Update(int id, EditPostDto dto)
|
||||
{
|
||||
var post = await postService.GetById(id);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != post.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
post.Title = dto.Title;
|
||||
if (!string.IsNullOrEmpty(dto.Description))
|
||||
post.Description = dto.Description;
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
var maybeTags = await TagNamesToTags(dto.Tags);
|
||||
if (maybeTags.Value != null)
|
||||
{
|
||||
post.Tags = maybeTags.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(maybeTags.Result != null);
|
||||
return maybeTags.Result;
|
||||
}
|
||||
}
|
||||
|
||||
var updated = await postService.Update(post);
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = updated.Id, fileId = updated.File.Id }, Request.Scheme
|
||||
);
|
||||
return Ok(PostDto.FromPost(updated, fileUrl));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("{postId:int}/comments")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PostDto>> CreateComment(int postId, CreateCommentDto dto)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var user = await userService.GetUserById(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
|
||||
var created = await commentService.Create(dto.Text, user, post);
|
||||
return CreatedAtAction(nameof(GetComment), new {postId = postId, commentId = created.Id}, CommentDto.FromComment(created));
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CommentDto>> GetComment(int postId, int commentId)
|
||||
{
|
||||
var entry = await postService.GetById(postId);
|
||||
if (entry == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
return Ok(CommentDto.FromComment(comment));
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/comments")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PagedList<CommentDto>>> GetAllComments(
|
||||
int postId,
|
||||
[Range(1, int.MaxValue)] int pageNumber = 1
|
||||
)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var list = await commentService.GetAll(pageNumber);
|
||||
var newItems = list.Items.Select(CommentDto.FromComment).ToList();
|
||||
return Ok(new PagedList<CommentDto>(newItems, list.CurrentPage, list.PageSize, list.TotalCount));
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteComment(int postId, int commentId)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != comment.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
var deleted = await commentService.Delete(comment);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPatch("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CommentDto>> Update(int postId, int commentId, EditCommentDto dto)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != comment.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
comment.Text = dto.Text;
|
||||
|
||||
var updated = await commentService.Update(comment);
|
||||
return Ok(CommentDto.FromComment(updated));
|
||||
}
|
||||
}
|
||||
78
T120B165-ImgBoard/Controllers/TagController.cs
Normal file
78
T120B165-ImgBoard/Controllers/TagController.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos;
|
||||
using T120B165_ImgBoard.Dtos.Tag;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/tags")]
|
||||
public class TagController(ITagService tagService) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<Tag>> Create(CreateTagDto dto)
|
||||
{
|
||||
// Check if tag exists, if it does, throw a conflict
|
||||
var existingTag = await tagService.GetByName(dto.Name);
|
||||
if (existingTag != null)
|
||||
{
|
||||
return Problem(
|
||||
detail: "Tag with such name already exists.",
|
||||
statusCode: StatusCodes.Status409Conflict
|
||||
);
|
||||
}
|
||||
|
||||
var createdTag = await tagService.Create(dto.Type, dto.Name);
|
||||
return CreatedAtAction(nameof(Get), new { name = createdTag.Name }, createdTag);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedList<Tag>>> GetAll(int pageNumber = 1)
|
||||
{
|
||||
return Ok(await tagService.GetAll(pageNumber));
|
||||
}
|
||||
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<Tag>> Get(string name)
|
||||
{
|
||||
var tag = await tagService.GetByName(name);
|
||||
if (tag == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(tag);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Delete(string name)
|
||||
{
|
||||
var deleted = await tagService.DeleteByName(name);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPatch("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<Tag>> Update(string name, EditTagDto dto)
|
||||
{
|
||||
var tag = await tagService.GetByName(name);
|
||||
if (tag == null) return NotFound();
|
||||
var updatedTag = await tagService.Update(tag, dto.Type);
|
||||
return Ok(updatedTag);
|
||||
}
|
||||
}
|
||||
20
T120B165-ImgBoard/Data/DbInitializer.cs
Normal file
20
T120B165-ImgBoard/Data/DbInitializer.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Data;
|
||||
|
||||
public static class DbInitializer
|
||||
{
|
||||
public static async Task SeedRolesAsync(IServiceProvider serviceProvider)
|
||||
{
|
||||
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||
string[] roleNames = [UserRoles.Admin, UserRoles.Regular];
|
||||
foreach (var roleName in roleNames)
|
||||
{
|
||||
if (!await roleManager.RoleExistsAsync(roleName))
|
||||
{
|
||||
await roleManager.CreateAsync(new IdentityRole(roleName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
T120B165-ImgBoard/Data/ImgBoardContext.cs
Normal file
29
T120B165-ImgBoard/Data/ImgBoardContext.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using File = T120B165_ImgBoard.Models.File;
|
||||
|
||||
namespace T120B165_ImgBoard.Data;
|
||||
|
||||
public class ImgBoardContext(DbContextOptions<ImgBoardContext> options) : IdentityDbContext<User>(options)
|
||||
{
|
||||
public DbSet<RefreshToken> RefreshTokens { get; set; }
|
||||
|
||||
public DbSet<Tag> Tags { get; set; }
|
||||
public DbSet<Post> Posts { get; set; }
|
||||
public DbSet<Comment> Comments { get; set; }
|
||||
public DbSet<File> Files { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<RefreshToken>().ToTable("RefreshTokens");
|
||||
|
||||
modelBuilder.Entity<Tag>().ToTable("Tags");
|
||||
modelBuilder.Entity<Post>().ToTable("Posts");
|
||||
modelBuilder.Entity<File>().ToTable("Files");
|
||||
modelBuilder.Entity<Comment>().ToTable("Comments");
|
||||
}
|
||||
}
|
||||
25
T120B165-ImgBoard/Dockerfile
Normal file
25
T120B165-ImgBoard/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["T120B165-ImgBoard/T120B165-ImgBoard.csproj", "T120B165-ImgBoard/"]
|
||||
RUN dotnet restore "T120B165-ImgBoard/T120B165-ImgBoard.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/T120B165-ImgBoard"
|
||||
RUN dotnet build "./T120B165-ImgBoard.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./T120B165-ImgBoard.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir -p /app/Storage && chown -R app:app /app/Storage
|
||||
VOLUME /app/Storage
|
||||
ENTRYPOINT ["dotnet", "T120B165-ImgBoard.dll"]
|
||||
13
T120B165-ImgBoard/Dtos/Comment/CommentDto.cs
Normal file
13
T120B165-ImgBoard/Dtos/Comment/CommentDto.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace T120B165_ImgBoard.Dtos.Comment;
|
||||
|
||||
public record CommentDto(int Id, string Text, SlimUserDto Author)
|
||||
{
|
||||
public static CommentDto FromComment(Models.Comment comment)
|
||||
{
|
||||
return new CommentDto(
|
||||
Id: comment.Id,
|
||||
Text: comment.Text,
|
||||
Author: SlimUserDto.FromUser(comment.Author)
|
||||
);
|
||||
}
|
||||
}
|
||||
4
T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs
Normal file
4
T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace T120B165_ImgBoard.Dtos.Comment;
|
||||
|
||||
public record CreateCommentDto(string Text);
|
||||
public record EditCommentDto(string Text);
|
||||
3
T120B165-ImgBoard/Dtos/LoginDto.cs
Normal file
3
T120B165-ImgBoard/Dtos/LoginDto.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record LoginDto(string Email, string Password);
|
||||
34
T120B165-ImgBoard/Dtos/Post/CreatePostDto.cs
Normal file
34
T120B165-ImgBoard/Dtos/Post/CreatePostDto.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos.Post;
|
||||
|
||||
public record CreatePostDto(string Title,
|
||||
[Required]
|
||||
string Description,
|
||||
[Required]
|
||||
List<string> Tags,
|
||||
[Required]
|
||||
string FileName,
|
||||
[Required]
|
||||
string FileMimeType,
|
||||
[Required]
|
||||
[Range(0, long.MaxValue, ErrorMessage = "The {0} must be a valid number and at least {1}.")]
|
||||
long? FileSize
|
||||
);
|
||||
|
||||
public record EditPostDto(string? Title, string? Description, List<string>? Tags): IValidatableObject
|
||||
{
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
var allEmpty = string.IsNullOrWhiteSpace(Title) &&
|
||||
string.IsNullOrWhiteSpace(Description) &&
|
||||
Tags == null;
|
||||
if (allEmpty)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"You must provide at least one value to edit: Title, Description, or Tags.",
|
||||
[nameof(Title), nameof(Description), nameof(Tags)]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
T120B165-ImgBoard/Dtos/Post/PostDto.cs
Normal file
25
T120B165-ImgBoard/Dtos/Post/PostDto.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using File = T120B165_ImgBoard.Models.File;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos.Post;
|
||||
|
||||
public record PostDto(
|
||||
int Id,
|
||||
string Title,
|
||||
string Description,
|
||||
SlimUserDto Author,
|
||||
List<Models.Tag> Tags,
|
||||
string? FileUrl
|
||||
)
|
||||
{
|
||||
public static PostDto FromPost(Models.Post post, string? fileUrl)
|
||||
{
|
||||
return new PostDto(
|
||||
Id: post.Id,
|
||||
Title: post.Title,
|
||||
Description: post.Description,
|
||||
Author: SlimUserDto.FromUser(post.Author),
|
||||
Tags: post.Tags,
|
||||
FileUrl: fileUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
3
T120B165-ImgBoard/Dtos/RefreshDto.cs
Normal file
3
T120B165-ImgBoard/Dtos/RefreshDto.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record RefreshDto(string RefreshToken);
|
||||
10
T120B165-ImgBoard/Dtos/RegisterDto.cs
Normal file
10
T120B165-ImgBoard/Dtos/RegisterDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record RegisterDto(
|
||||
string UserName,
|
||||
[EmailAddress]
|
||||
string Email,
|
||||
string Password
|
||||
);
|
||||
11
T120B165-ImgBoard/Dtos/SlimUserDto.cs
Normal file
11
T120B165-ImgBoard/Dtos/SlimUserDto.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record SlimUserDto(string UserId, string UserName)
|
||||
{
|
||||
public static SlimUserDto FromUser(User user)
|
||||
{
|
||||
return new SlimUserDto(user.Id, user.UserName);
|
||||
}
|
||||
};
|
||||
14
T120B165-ImgBoard/Dtos/Tag/CreateTagDto.cs
Normal file
14
T120B165-ImgBoard/Dtos/Tag/CreateTagDto.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos.Tag;
|
||||
|
||||
public record CreateTagDto(
|
||||
[Required]
|
||||
TagType Type,
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
string Name
|
||||
);
|
||||
|
||||
public record EditTagDto([Required] TagType Type);
|
||||
3
T120B165-ImgBoard/Dtos/TokenDto.cs
Normal file
3
T120B165-ImgBoard/Dtos/TokenDto.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record TokenDto(string AccessToken, string RefreshToken);
|
||||
5
T120B165-ImgBoard/Dtos/UserDto.cs
Normal file
5
T120B165-ImgBoard/Dtos/UserDto.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Dtos;
|
||||
|
||||
public record UserDto(int Id, string Name, string Email);
|
||||
16
T120B165-ImgBoard/Models/Comment.cs
Normal file
16
T120B165-ImgBoard/Models/Comment.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace T120B165_ImgBoard.Models;
|
||||
|
||||
public class Comment
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
public string Text { get; set; }
|
||||
public User Author { get; set; }
|
||||
public Post OriginalPost { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? Updated { get; set; }
|
||||
}
|
||||
24
T120B165-ImgBoard/Models/File.cs
Normal file
24
T120B165-ImgBoard/Models/File.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace T120B165_ImgBoard.Models;
|
||||
|
||||
public class File
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
// The physical path where the file is stored
|
||||
public required string FilePath { get; set; }
|
||||
// Original file name uploaded by the user
|
||||
public required string OriginalFileName { get; set; }
|
||||
// The size of the file in bytes
|
||||
public required long Size { get; set; }
|
||||
// The MIME type, will be validated at the end of the upload
|
||||
public required string ContentType { get; set; }
|
||||
// Tracks when the file metadata was created/upload process started
|
||||
public required DateTime CreatedDate { get; set; }
|
||||
// Tracks when the file was successfully finalized
|
||||
// Null until the final PATCH request completes
|
||||
public DateTime? FinishedDate { get; set; }
|
||||
}
|
||||
17
T120B165-ImgBoard/Models/Post.cs
Normal file
17
T120B165-ImgBoard/Models/Post.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace T120B165_ImgBoard.Models;
|
||||
|
||||
public class Post
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public User Author { get; set; }
|
||||
public required DateTime Created { get; set; }
|
||||
public required List<Tag> Tags { get; set; }
|
||||
public required File File { get; set; }
|
||||
}
|
||||
17
T120B165-ImgBoard/Models/Tag.cs
Normal file
17
T120B165-ImgBoard/Models/Tag.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace T120B165_ImgBoard.Models;
|
||||
|
||||
public enum TagType
|
||||
{
|
||||
General,
|
||||
Copyright,
|
||||
}
|
||||
|
||||
public class Tag
|
||||
{
|
||||
[Key]
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required TagType Type { get; set; }
|
||||
}
|
||||
23
T120B165-ImgBoard/Models/User.cs
Normal file
23
T120B165-ImgBoard/Models/User.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace T120B165_ImgBoard.Models;
|
||||
|
||||
public static class UserRoles
|
||||
{
|
||||
public const string Admin = "Admin";
|
||||
public const string Regular = "Regular";
|
||||
}
|
||||
|
||||
public class RefreshToken
|
||||
{
|
||||
[Key]
|
||||
public string Token { get; set; }
|
||||
public User User { get; set; }
|
||||
public DateTime Expires { get; set; }
|
||||
}
|
||||
|
||||
public class User : IdentityUser
|
||||
{
|
||||
//public List<Post> Posts { get; set; }
|
||||
};
|
||||
108
T120B165-ImgBoard/Program.cs
Normal file
108
T120B165-ImgBoard/Program.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
|
||||
namespace T120B165_ImgBoard;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddControllers().AddNewtonsoftJson(
|
||||
options => options.SerializerSettings.Converters.Add(new StringEnumConverter()));
|
||||
builder.Services.AddOpenApiDocument();
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
|
||||
builder.Services.AddIdentity<User, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ImgBoardContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
|
||||
builder.Services.AddScoped<ITagService, TagService>();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<IPostService, PostService>();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<ICommentService, CommentService>();
|
||||
builder.Services.AddScoped<IFileService, FileService>();
|
||||
|
||||
// If we're in dev environment, use in-memory database
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.Services.AddDbContext<ImgBoardContext>(options =>
|
||||
options.UseInMemoryDatabase("T120B165_ImgBoard"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Running in production mode");
|
||||
builder.Services.AddDbContext<ImgBoardContext>(options =>
|
||||
options.UseMySql(
|
||||
builder.Configuration.GetConnectionString("DbContext"),
|
||||
new MySqlServerVersion(new Version(11, 8, 3))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt");
|
||||
var key = Encoding.UTF8.GetBytes(jwtSettings["Key"]);
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = jwtSettings["Issuer"],
|
||||
ValidAudience = jwtSettings["Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key)
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseOpenApi();
|
||||
app.UseSwaggerUi();
|
||||
}
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
|
||||
var context = services.GetRequiredService<ImgBoardContext>();
|
||||
context.Database.EnsureCreated();
|
||||
DbInitializer.SeedRolesAsync(services).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
23
T120B165-ImgBoard/Properties/launchSettings.json
Normal file
23
T120B165-ImgBoard/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5259",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7261;http://localhost:5259",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
T120B165-ImgBoard/Services/CommentService.cs
Normal file
68
T120B165-ImgBoard/Services/CommentService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface ICommentService
|
||||
{
|
||||
Task<Comment> Create(string text, User author, Post post);
|
||||
Task<Comment?> GetById(int commentId);
|
||||
Task<PagedList<Comment>> GetAll(int pageNumber = 1);
|
||||
Task<bool> Delete(Comment comment);
|
||||
Task<Comment> Update(Comment comment);
|
||||
}
|
||||
|
||||
public class CommentService(ImgBoardContext context): ICommentService
|
||||
{
|
||||
private const int PageSize = 20;
|
||||
|
||||
public async Task<Comment> Create(string text, User author, Post post)
|
||||
{
|
||||
var entry = new Comment
|
||||
{
|
||||
Text = text,
|
||||
Author = author,
|
||||
OriginalPost = post,
|
||||
Created = DateTime.Now,
|
||||
};
|
||||
await context.Comments.AddAsync(entry);
|
||||
await context.SaveChangesAsync();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async Task<Comment?> GetById(int commentId)
|
||||
{
|
||||
return await context.Comments.Where(t => t.Id == commentId)
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.OriginalPost)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<Comment>> GetAll(int pageNumber = 1)
|
||||
{
|
||||
var totalCount = await context.Comments.CountAsync();
|
||||
var items = await context.Comments
|
||||
.Skip((pageNumber - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.OriginalPost)
|
||||
.ToListAsync();
|
||||
return new PagedList<Comment>(items, pageNumber, PageSize, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> Delete(Comment comment)
|
||||
{
|
||||
context.Comments.Remove(comment);
|
||||
await context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Comment> Update(Comment comment)
|
||||
{
|
||||
context.Comments.Update(comment);
|
||||
await context.SaveChangesAsync();
|
||||
return comment;
|
||||
}
|
||||
}
|
||||
34
T120B165-ImgBoard/Services/FileService.cs
Normal file
34
T120B165-ImgBoard/Services/FileService.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using File = T120B165_ImgBoard.Models.File;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
public Task<File?> GetFileById(long id);
|
||||
Task<File> Update(File file);
|
||||
Task Delete(File file);
|
||||
}
|
||||
|
||||
public class FileService(ImgBoardContext context): IFileService
|
||||
{
|
||||
public async Task<File?> GetFileById(long id)
|
||||
{
|
||||
return await context.Files.Where(f => f.Id == id).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<File> Update(File file)
|
||||
{
|
||||
context.Files.Update(file);
|
||||
await context.SaveChangesAsync();
|
||||
return file;
|
||||
}
|
||||
|
||||
public async Task Delete(File file)
|
||||
{
|
||||
var actualFile = await GetFileById(file.Id);
|
||||
context.Files.Remove(actualFile);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
100
T120B165-ImgBoard/Services/PostService.cs
Normal file
100
T120B165-ImgBoard/Services/PostService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
using File = T120B165_ImgBoard.Models.File;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface IPostService
|
||||
{
|
||||
Task<CreatedPost> Create(
|
||||
string title, string description, List<Tag> tags, User author,
|
||||
string fileName, string fileContentType, long fileSize);
|
||||
Task<Post?> GetById(int postId, bool includeUnfinished = false);
|
||||
Task<PagedList<Post>> GetAll(int pageNumber = 1);
|
||||
Task<bool> Delete(Post post);
|
||||
Task<Post> Update(Post post);
|
||||
}
|
||||
|
||||
public record CreatedPost(Post Post, File File);
|
||||
|
||||
public class PostService(ImgBoardContext context): IPostService
|
||||
{
|
||||
private const int PageSize = 20;
|
||||
public async Task<CreatedPost> Create(
|
||||
string title,
|
||||
string description,
|
||||
List<Tag> tags,
|
||||
User author,
|
||||
string fileName,
|
||||
string fileContentType,
|
||||
long fileSize
|
||||
)
|
||||
{
|
||||
var file = new File
|
||||
{
|
||||
OriginalFileName = fileName,
|
||||
Size = fileSize,
|
||||
ContentType = fileContentType,
|
||||
FilePath = Path.GetTempFileName(),
|
||||
CreatedDate = DateTime.Now,
|
||||
};
|
||||
await context.Files.AddAsync(file);
|
||||
|
||||
var entry = new Post
|
||||
{
|
||||
Title = title,
|
||||
Description = description,
|
||||
Author = author,
|
||||
Created = DateTime.Now,
|
||||
Tags = tags,
|
||||
File = file
|
||||
};
|
||||
await context.Posts.AddAsync(entry);
|
||||
await context.SaveChangesAsync();
|
||||
return new CreatedPost(entry, file);
|
||||
}
|
||||
|
||||
public async Task<Post?> GetById(int postId, bool includeUnfinished = false)
|
||||
{
|
||||
var query = context.Posts.Where(t => t.Id == postId);
|
||||
if (!includeUnfinished)
|
||||
{
|
||||
query = query.Where(t => t.File.FinishedDate != null);
|
||||
}
|
||||
|
||||
return await query.Include(b => b.Author)
|
||||
.Include(b => b.Tags)
|
||||
.Include(b => b.File)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<Post>> GetAll(int pageNumber = 1)
|
||||
{
|
||||
var totalCount = await context.Posts.Where(p => p.File.FinishedDate != null).CountAsync();
|
||||
var items = await context.Posts
|
||||
.Skip((pageNumber - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.Where(p => p.File.FinishedDate != null)
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.Tags)
|
||||
.Include(b => b.File)
|
||||
.ToListAsync();
|
||||
return new PagedList<Post>(items, pageNumber, PageSize, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> Delete(Post post)
|
||||
{
|
||||
context.Posts.Remove(post);
|
||||
await context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Post> Update(Post post)
|
||||
{
|
||||
context.Posts.Update(post);
|
||||
await context.SaveChangesAsync();
|
||||
return post;
|
||||
}
|
||||
}
|
||||
68
T120B165-ImgBoard/Services/TagService.cs
Normal file
68
T120B165-ImgBoard/Services/TagService.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Dtos;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface ITagService
|
||||
{
|
||||
Task<Tag?> GetByName(string name);
|
||||
|
||||
Task<bool> DeleteByName(string name);
|
||||
|
||||
Task<PagedList<Tag>> GetAll(int pageNumber = 1);
|
||||
|
||||
Task<Tag> Update(Tag tag, TagType type);
|
||||
|
||||
Task<Tag> Create(TagType type, string name);
|
||||
}
|
||||
|
||||
public class TagService(ImgBoardContext context): ITagService
|
||||
{
|
||||
public async Task<Tag?> GetByName(string name)
|
||||
{
|
||||
return await context.Tags.Where(t => t.Name == name).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<PagedList<Tag>> GetAll(int pageNumber)
|
||||
{
|
||||
int pageSize = 20;
|
||||
var totalCount = await context.Tags.CountAsync();
|
||||
var products = await context.Tags
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
return new PagedList<Tag>(products, pageNumber, pageSize, totalCount);;
|
||||
}
|
||||
|
||||
public async Task<Tag> Update(Tag tag, TagType type)
|
||||
{
|
||||
tag.Type = type;
|
||||
context.Tags.Update(tag);
|
||||
await context.SaveChangesAsync();
|
||||
return tag;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteByName(string name)
|
||||
{
|
||||
var tag = await context.Tags.FindAsync(name);
|
||||
if (tag == null) return false;
|
||||
context.Tags.Remove(tag);
|
||||
await context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Tag> Create(TagType type, string name)
|
||||
{
|
||||
var tag = new Tag
|
||||
{
|
||||
Name = name,
|
||||
Type = type,
|
||||
};
|
||||
await context.Tags.AddAsync(tag);
|
||||
await context.SaveChangesAsync();
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
74
T120B165-ImgBoard/Services/TokenService.cs
Normal file
74
T120B165-ImgBoard/Services/TokenService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
Task<string> GenerateJwtToken(User user);
|
||||
Task<string> GenerateRefreshToken(User user);
|
||||
|
||||
Task<RefreshToken?> GetRefreshTokenByValue(string refreshToken);
|
||||
Task<bool> InvalidateRefreshToken(RefreshToken refreshToken);
|
||||
}
|
||||
|
||||
public class TokenService(ImgBoardContext context, IConfiguration config, UserManager<User> userManager): ITokenService
|
||||
{
|
||||
|
||||
public async Task<RefreshToken?> GetRefreshTokenByValue(string refreshToken)
|
||||
{
|
||||
return await context.RefreshTokens.Where(t => t.Token == refreshToken && t.Expires > DateTime.Now)
|
||||
.Include(t => t.User)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> InvalidateRefreshToken(RefreshToken refreshToken)
|
||||
{
|
||||
context.RefreshTokens.Remove(refreshToken);
|
||||
await context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateJwtToken(User user)
|
||||
{
|
||||
var roles = await userManager.GetRolesAsync(user);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, user.Id),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new(JwtRegisteredClaimNames.Email, user.Email)
|
||||
};
|
||||
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:Key"]));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: config["Jwt:Issuer"],
|
||||
audience: config["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(Convert.ToDouble(config["Jwt:DurationInMinutes"])),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateRefreshToken(User user)
|
||||
{
|
||||
var token = new RefreshToken
|
||||
{
|
||||
Expires = DateTime.Now.AddDays(30),
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
User = user
|
||||
};
|
||||
await context.RefreshTokens.AddAsync(token);
|
||||
await context.SaveChangesAsync();
|
||||
return token.Token;
|
||||
}
|
||||
}
|
||||
18
T120B165-ImgBoard/Services/UserService.cs
Normal file
18
T120B165-ImgBoard/Services/UserService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using T120B165_ImgBoard.Data;
|
||||
using T120B165_ImgBoard.Models;
|
||||
|
||||
namespace T120B165_ImgBoard.Services;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
public Task<User?> GetUserById(string id);
|
||||
}
|
||||
|
||||
public class UserService(ImgBoardContext context): IUserService
|
||||
{
|
||||
public async Task<User?> GetUserById(string id)
|
||||
{
|
||||
return await context.Users.Where(u => u.Id == id).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
30
T120B165-ImgBoard/T120B165-ImgBoard.csproj
Normal file
30
T120B165-ImgBoard/T120B165-ImgBoard.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>T120B165_ImgBoard</RootNamespace>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FileSignatures" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.9" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NSwag.AspNetCore" Version="14.5.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
11
T120B165-ImgBoard/Utils/PagedList.cs
Normal file
11
T120B165-ImgBoard/Utils/PagedList.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace T120B165_ImgBoard.Utils;
|
||||
|
||||
public class PagedList<T>(List<T> items, int pageNumber, int pageSize, int totalCount)
|
||||
{
|
||||
public int CurrentPage { get; } = pageNumber;
|
||||
public int PageSize { get; } = pageSize;
|
||||
public int TotalCount { get; } = totalCount;
|
||||
public int TotalPages => (int) Math.Ceiling((double)TotalCount / PageSize);
|
||||
|
||||
public List<T> Items { get; } = items;
|
||||
}
|
||||
14
T120B165-ImgBoard/appsettings.Development.json
Normal file
14
T120B165-ImgBoard/appsettings.Development.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "your_secret_key_here_asta_lavista_baby",
|
||||
"Issuer": "your_app",
|
||||
"Audience": "your_app_users",
|
||||
"DurationInMinutes": 60
|
||||
}
|
||||
}
|
||||
29
compose.yaml
Normal file
29
compose.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
t120b165-imgboard:
|
||||
image: t120b165-imgboard
|
||||
container_name: t120b165_app
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: T120B165-ImgBoard/Dockerfile
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- app_data:/app/Storage
|
||||
db:
|
||||
image: mariadb:lts
|
||||
container_name: t120b165_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: supersecretpassword
|
||||
MARIADB_DATABASE: imgboard_db
|
||||
MARIADB_USER: imgboard
|
||||
MARIADB_PASSWORD: supersecretpassword
|
||||
volumes:
|
||||
- ./db_data:/var/lib/mysql
|
||||
|
||||
volumes:
|
||||
app_data:
|
||||
driver: local
|
||||
1482
swagger.json
Normal file
1482
swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user