Compare commits
257 commits
Author | SHA1 | Date | |
---|---|---|---|
b0431ff962 | |||
b07f4b75c0 | |||
22be49976a | |||
3527acb8ba | |||
978b8e100e | |||
f00f5b400e | |||
f5f0416346 | |||
5d452824cd | |||
bba322bd22 | |||
200e648772 | |||
790b39f730 | |||
7d0df67c06 | |||
dd9d35249c | |||
f99d10ecf0 | |||
7759225428 | |||
cd24196cd1 | |||
7d6d4631b8 | |||
a248536789 | |||
218c756a70 | |||
7ea6c62d67 | |||
64ea25e89e | |||
f1f777ff82 | |||
a72c0f41c3 | |||
6fe816404f | |||
d1faf1ddee | |||
92bf933c10 | |||
c8e4078b35 | |||
0c6e3bf38f | |||
30146556f5 | |||
c47fc41437 | |||
373d97e70a | |||
74800b46ef | |||
32e0c09d06 | |||
6bb01f0bf1 | |||
cacd3a30b7 | |||
a0ba712632 | |||
83b62b4845 | |||
045964ffb7 | |||
8edbc8bf1d | |||
db22e35f0d | |||
9d3d46bf33 | |||
12eddb9949 | |||
8713279d3d | |||
dc9c11ec52 | |||
53006ea313 | |||
49e9eabea0 | |||
5077bd6a0b | |||
3f0edc4374 | |||
7468aa20ab | |||
fe1cf7ce8a | |||
478ba2a406 | |||
78afb8b9c4 | |||
e908e67ca6 | |||
d182b07482 | |||
2281b3e478 | |||
140419a1ca | |||
7791c91960 | |||
5e7df2e074 | |||
e24c4f9b00 | |||
3f8f6d0f23 | |||
661c3eab0f | |||
96725cc304 | |||
8a2ffd7d69 | |||
546e900204 | |||
bd21eeebcf | |||
05913a3b2f | |||
1fb1d8dd14 | |||
ddd96e415a | |||
397ffc2d5e | |||
80385893c7 | |||
d518cdf739 | |||
27846a4fe4 | |||
f766a2054b | |||
36cb1d2043 | |||
79b8c4799e | |||
b36b54f9e6 | |||
507b9c3f4c | |||
41a008799a | |||
11257ae069 | |||
49b2902d6d | |||
9d33093339 | |||
26b32b40e2 | |||
5cdadc6158 | |||
39a3098a99 | |||
1cf2619393 | |||
77c3047b1e | |||
51e335f090 | |||
1ce4f9d278 | |||
ff8d53814d | |||
5cb3faa92b | |||
7f8e72e857 | |||
a9ccc12671 | |||
a29d1fdb78 | |||
7e6698c3fb | |||
80b7f192f1 | |||
3338243cea | |||
d30ebacc72 | |||
8a8b4caa18 | |||
bb2fa55cd5 | |||
c6eba5b51a | |||
b0a286dd9f | |||
2a0df335bc | |||
d9d48c3cbf | |||
8bd4449804 | |||
f8e6032449 | |||
649988db25 | |||
bc7fd6d804 | |||
57e1ec09c0 | |||
03209e4028 | |||
9966656c0c | |||
c20831f20d | |||
74222ead45 | |||
71d3b42330 | |||
18bdbc0745 | |||
903be2709c | |||
f0ae648492 | |||
54be457a47 | |||
b47ed7b699 | |||
02e2b230bf | |||
f3bb2d5d01 | |||
de733a0682 | |||
4780be3019 | |||
8b1d5b2c1b | |||
71b59dbb00 | |||
f435ad4cf5 | |||
7c52ab759c | |||
59496a8cd8 | |||
b6d42fb15d | |||
004111feb6 | |||
c237aa8827 | |||
c0bb76580d | |||
8bba5f6137 | |||
261435c252 | |||
c179669799 | |||
0c78cd25b0 | |||
0d47f1fb01 | |||
c8cd483d20 | |||
7cb17409cd | |||
4e9c4af4a5 | |||
142ff36d3a | |||
d87856bf2c | |||
6abf505c40 | |||
d0bf638a21 | |||
9160281ea2 | |||
201c56c3dd | |||
c4cb08cdc1 | |||
5a22807410 | |||
d982342ab8 | |||
0077a165b5 | |||
a4ca0902a3 | |||
567e794154 | |||
40da4865bc | |||
e030342358 | |||
5b17c716cb | |||
7f971e8549 | |||
5fab66444f | |||
06f7019330 | |||
eac0a17473 | |||
aa756ac56a | |||
42041d49bc | |||
c18b79e570 | |||
9b55747657 | |||
3f8fe307ab | |||
2a66e3e25e | |||
5a8b7aae80 | |||
b1165c3780 | |||
562ecc46bd | |||
4002893323 | |||
80ac16694c | |||
8f3478d57a | |||
2b8e4c3e8d | |||
646c2694e1 | |||
19bfee6203 | |||
0bdd0148d2 | |||
3f0a94af3d | |||
4514216405 | |||
dc18ab60d2 | |||
f539902711 | |||
e11e60e16b | |||
8fe8755183 | |||
a3cbdc1a08 | |||
6a4aa8064a | |||
758ab9ec5b | |||
e20a7d3465 | |||
14e6e35cb7 | |||
ff2ba1fb1b | |||
a70078995b | |||
39b0917585 | |||
e83895255e | |||
b5f9ef9bd6 | |||
e76c634738 | |||
e7e4937082 | |||
df93f28273 | |||
6ea8861da2 | |||
a4a62fa6b6 | |||
6c7a26c73a | |||
6f79d35f11 | |||
f81ae97821 | |||
bb649d1d72 | |||
4732451040 | |||
4ba28bbfde | |||
0f3ab19f6f | |||
862a64840e | |||
412d720abc | |||
41e620ad03 | |||
6388e3127d | |||
bb76c24017 | |||
0f51f01b34 | |||
6acd9b94f4 | |||
df09a2add8 | |||
cf2f624ae4 | |||
821712f43b | |||
2cef7523d2 | |||
103ba24555 | |||
ff22530f0a | |||
116d0577a7 | |||
4ac0001795 | |||
2682cabfb0 | |||
be34c4c77e | |||
498d79de4e | |||
2323810b06 | |||
8054d68f79 | |||
3d22385689 | |||
13a0cac663 | |||
c77ee660ca | |||
344a0071e5 | |||
acc54a55bc | |||
fa71f3fb23 | |||
c4adf6918c | |||
fa3c1ccaa7 | |||
22d09ad7a6 | |||
6c9d1c328b | |||
fb324e7576 | |||
54ec469cd9 | |||
4a6b5f3b85 | |||
0aadc5fb47 | |||
2915893049 | |||
ef221b2c45 | |||
c4e39d4d59 | |||
2b91723696 | |||
a069d0ff15 | |||
fb34464199 | |||
e7ec0e6661 | |||
16f230b97d | |||
fa49030b06 | |||
e95e0a79ff | |||
d6c9345dba | |||
a7950671e1 | |||
6186eda092 | |||
2a7bd746aa | |||
25540f2de2 | |||
493a6e4d29 | |||
50257d61f8 | |||
a2f001392b | |||
14f8e77e6a | |||
401e268281 | |||
24155a149c |
417 changed files with 32377 additions and 2581 deletions
20
.config/dotnet-tools.json
Normal file
20
.config/dotnet-tools.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"husky": {
|
||||
"version": "0.7.2",
|
||||
"commands": [
|
||||
"husky"
|
||||
],
|
||||
"rollForward": false
|
||||
},
|
||||
"csharpier": {
|
||||
"version": "0.30.6",
|
||||
"commands": [
|
||||
"dotnet-csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
24
.dockerignore
Normal file
24
.dockerignore
Normal file
|
@ -0,0 +1,24 @@
|
|||
**/.dockerignore
|
||||
**/.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
|
||||
LICENSE
|
||||
README.md
|
||||
static-pages/*
|
|
@ -1,2 +1,52 @@
|
|||
[*.cs]
|
||||
[*]
|
||||
# We use PostgresSQL which doesn't recommend more specific string types
|
||||
resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none
|
||||
# This is raised for every single property of records returned by endpoints
|
||||
resharper_not_accessed_positional_property_local_highlighting = none
|
||||
|
||||
|
||||
# Microsoft .NET properties
|
||||
csharp_new_line_before_members_in_object_initializers = false
|
||||
csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion
|
||||
|
||||
# ReSharper properties
|
||||
resharper_align_multiline_binary_expressions_chain = false
|
||||
resharper_arguments_skip_single = true
|
||||
resharper_blank_lines_after_start_comment = 0
|
||||
resharper_blank_lines_around_single_line_invocable = 0
|
||||
resharper_blank_lines_before_block_statements = 0
|
||||
resharper_braces_for_foreach = required_for_multiline
|
||||
resharper_braces_for_ifelse = required_for_multiline
|
||||
resharper_braces_redundant = false
|
||||
resharper_csharp_blank_lines_around_field = 0
|
||||
resharper_csharp_empty_block_style = together_same_line
|
||||
resharper_csharp_max_line_length = 166
|
||||
resharper_csharp_wrap_after_declaration_lpar = true
|
||||
resharper_csharp_wrap_before_binary_opsign = true
|
||||
resharper_csharp_wrap_before_declaration_rpar = true
|
||||
resharper_csharp_wrap_parameters_style = chop_if_long
|
||||
resharper_indent_preprocessor_other = do_not_change
|
||||
resharper_instance_members_qualify_declared_in =
|
||||
resharper_keep_existing_attribute_arrangement = true
|
||||
resharper_max_attribute_length_for_same_line = 70
|
||||
resharper_place_accessorholder_attribute_on_same_line = false
|
||||
resharper_place_expr_method_on_single_line = if_owner_is_single_line
|
||||
resharper_place_method_attribute_on_same_line = if_owner_is_single_line
|
||||
resharper_place_record_field_attribute_on_same_line = true
|
||||
resharper_place_simple_embedded_statement_on_same_line = false
|
||||
resharper_place_simple_initializer_on_single_line = false
|
||||
resharper_place_simple_list_pattern_on_single_line = false
|
||||
resharper_space_within_empty_braces = false
|
||||
resharper_trailing_comma_in_multiline_lists = true
|
||||
resharper_wrap_after_invocation_lpar = false
|
||||
resharper_wrap_before_invocation_rpar = false
|
||||
resharper_wrap_before_primary_constructor_declaration_rpar = true
|
||||
resharper_wrap_chained_binary_patterns = chop_if_long
|
||||
resharper_wrap_list_pattern = chop_always
|
||||
resharper_wrap_object_and_collection_initializer_style = chop_always
|
||||
|
||||
# Roslynator properties
|
||||
dotnet_diagnostic.RCS1194.severity = none
|
||||
|
||||
[*generated.cs]
|
||||
generated_code = true
|
16
.gitignore
vendored
16
.gitignore
vendored
|
@ -1,3 +1,19 @@
|
|||
bin/
|
||||
obj/
|
||||
node_modules/
|
||||
.version
|
||||
config.ini
|
||||
*.DotSettings.user
|
||||
proxy-config.json
|
||||
.DS_Store
|
||||
.idea/.idea.Foxnouns.NET/.idea/dataSources.xml
|
||||
.idea/.idea.Foxnouns.NET/.idea/sqldialects.xml
|
||||
|
||||
docker/config.ini
|
||||
docker/proxy-config.json
|
||||
docker/frontend.env
|
||||
|
||||
Foxnouns.DataMigrator/apps.json
|
||||
|
||||
out/
|
||||
build/
|
||||
|
|
22
.husky/pre-commit
Executable file
22
.husky/pre-commit
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
## husky task runner examples -------------------
|
||||
## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky'
|
||||
|
||||
## run all tasks
|
||||
#husky run
|
||||
|
||||
### run all tasks with group: 'group-name'
|
||||
#husky run --group group-name
|
||||
|
||||
## run task with name: 'task-name'
|
||||
#husky run --name task-name
|
||||
|
||||
## pass hook arguments to task
|
||||
#husky run --args "$1" "$2"
|
||||
|
||||
## or put your custom commands -------------------
|
||||
#echo 'Husky.Net is awesome!'
|
||||
|
||||
dotnet husky run
|
34
.husky/task-runner.json
Normal file
34
.husky/task-runner.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"$schema": "https://alirezanet.github.io/Husky.Net/schema.json",
|
||||
"tasks": [
|
||||
{
|
||||
"name": "run-prettier",
|
||||
"command": "pnpm",
|
||||
"args": [
|
||||
"prettier",
|
||||
"-w",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"Foxnouns.Frontend/**/*.ts",
|
||||
"Foxnouns.Frontend/**/*.json",
|
||||
"Foxnouns.Frontend/**/*.scss",
|
||||
"Foxnouns.Frontend/**/*.js",
|
||||
"Foxnouns.Frontend/**/*.svelte"
|
||||
],
|
||||
"cwd": "Foxnouns.Frontend/",
|
||||
"pathMode": "absolute"
|
||||
},
|
||||
{
|
||||
"name": "run-csharpier",
|
||||
"command": "dotnet",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"${staged}"
|
||||
],
|
||||
"include": [
|
||||
"**/*.cs"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
7
.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml
generated
Normal file
7
.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.intellij.csharpier">
|
||||
<option name="customPath" value="" />
|
||||
<option name="runOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
61
.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml
generated
Normal file
61
.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml
generated
Normal file
|
@ -0,0 +1,61 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<HTMLCodeStyleSettings>
|
||||
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</HTMLCodeStyleSettings>
|
||||
<JSCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</JSCodeStyleSettings>
|
||||
<TypeScriptCodeStyleSettings version="0">
|
||||
<option name="FORCE_SEMICOLON_STYLE" value="true" />
|
||||
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
|
||||
<option name="FORCE_QUOTE_STYlE" value="true" />
|
||||
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
|
||||
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
|
||||
<option name="SPACES_WITHIN_IMPORTS" value="true" />
|
||||
</TypeScriptCodeStyleSettings>
|
||||
<VueCodeStyleSettings>
|
||||
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
|
||||
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
|
||||
</VueCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JavaScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="TypeScript">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Vue">
|
||||
<option name="SOFT_MARGINS" value="100" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
5
.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
5
.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml
generated
5
.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml
generated
|
@ -1,7 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<attachedFolders>
|
||||
<Path>Foxnouns.Frontend</Path>
|
||||
<Path>migrators/go-exporter</Path>
|
||||
</attachedFolders>
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
|
|
40
.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
40
.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
@ -0,0 +1,40 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredUrls">
|
||||
<list>
|
||||
<option value="http://" />
|
||||
<option value="http://0.0.0.0" />
|
||||
<option value="http://127.0.0.1" />
|
||||
<option value="http://activemq.apache.org/schema/" />
|
||||
<option value="http://cxf.apache.org/schemas/" />
|
||||
<option value="http://java.sun.com/" />
|
||||
<option value="http://javafx.com/fxml" />
|
||||
<option value="http://javafx.com/javafx/" />
|
||||
<option value="http://json-schema.org/draft" />
|
||||
<option value="http://localhost" />
|
||||
<option value="http://maven.apache.org/POM/" />
|
||||
<option value="http://maven.apache.org/xsd/" />
|
||||
<option value="http://primefaces.org/ui" />
|
||||
<option value="http://schema.cloudfoundry.org/spring/" />
|
||||
<option value="http://schemas.xmlsoap.org/" />
|
||||
<option value="http://tiles.apache.org/" />
|
||||
<option value="http://www.ibm.com/webservices/xsd" />
|
||||
<option value="http://www.jboss.com/xml/ns/" />
|
||||
<option value="http://www.jboss.org/j2ee/schema/" />
|
||||
<option value="http://www.springframework.org/schema/" />
|
||||
<option value="http://www.springframework.org/security/tags" />
|
||||
<option value="http://www.springframework.org/tags" />
|
||||
<option value="http://www.thymeleaf.org" />
|
||||
<option value="http://www.w3.org/" />
|
||||
<option value="http://xmlns.jcp.org/" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="RequiredAttributes" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="myAdditionalRequiredHtmlAttributes" value="column" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
7
.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml
generated
Normal file
7
.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<files-pattern value="**/*.{js,ts,jsx,tsx,html,vue,svelte}" />
|
||||
<option name="fix-on-save" value="true" />
|
||||
</component>
|
||||
</project>
|
9
.idea/.idea.Foxnouns.NET/.idea/prettier.xml
generated
Normal file
9
.idea/.idea.Foxnouns.NET/.idea/prettier.xml
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
<option name="myRunOnSave" value="true" />
|
||||
<option name="myRunOnReformat" value="true" />
|
||||
<option name="myFilesPattern" value="**/*.{js,ts,jsx,tsx,vue,astro,svelte,html}" />
|
||||
</component>
|
||||
</project>
|
4
.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml
generated
Normal file
4
.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml
generated
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions" suppressed-tasks="SCSS" />
|
||||
</project>
|
0
.noai
Normal file
0
.noai
Normal file
32
DOCKER.md
Normal file
32
DOCKER.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)*
|
||||
|
||||
Because SvelteKit is a pain in the ass to build in a container, and processes secrets at build time,
|
||||
there is no pre-built frontend image available.
|
||||
If you don't want to build images on your server, I recommend running the frontend outside of Docker.
|
||||
This is preconfigured in `docker-compose.prebuilt.yml`: the backend, database, and rate limiter will run in Docker,
|
||||
while the frontend is run as a normal, non-containerized service.
|
||||
|
||||
1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking.
|
||||
2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same.
|
||||
3. Run with `docker compose up -f docker-compose.prebuilt.yml`
|
||||
|
||||
The backend will listen on port 5001 and metrics will be available on port 5002.
|
||||
The rate limiter (which is what should be exposed to the outside) will listen on port 5003.
|
||||
You can use `docker/Caddyfile` as an example for your reverse proxy. If you use nginx, good luck.
|
||||
|
||||
# Running with Docker (local builds)
|
||||
|
||||
In order to run *everything* in Docker, you'll have to build every container yourself.
|
||||
The advantage of this is that it's an all-in-one solution, where you only have to point your reverse proxy at a single container.
|
||||
The disadvantage is that you'll likely have to build the images on the server you'll be running them on.
|
||||
|
||||
1. Configure the backend and rate limiter as in the section above.
|
||||
2. Copy `docker/frontend.example.env` to `docker/frontend.env`, and configure it.
|
||||
3. Build with `docker compose build -f docker-compose.local.yml`
|
||||
4. Run with `docker compose up -f docker-compose.local.yml`
|
||||
|
||||
The Caddy server will listen on `localhost:5004` for the frontend and API,
|
||||
and on `localhost:5005` for the profile URL shortener.
|
||||
|
||||
The backend server listens on `localhost:5006` for unproxied API access,
|
||||
and `localhost:5007` for metrics.
|
22
Dockerfile.backend
Normal file
22
Dockerfile.backend
Normal file
|
@ -0,0 +1,22 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Foxnouns.Backend/Foxnouns.Backend.csproj", "Foxnouns.Backend/"]
|
||||
RUN dotnet restore "Foxnouns.Backend/Foxnouns.Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Foxnouns.Backend"
|
||||
RUN dotnet build "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Foxnouns.Backend.dll", "--migrate-and-start"]
|
48
ENDPOINTS.md
Normal file
48
ENDPOINTS.md
Normal file
|
@ -0,0 +1,48 @@
|
|||
# List of API endpoints and scopes
|
||||
|
||||
## Scopes
|
||||
|
||||
- `identify`: `@me` will refer to token user (always granted)
|
||||
- `user.read_hidden`: can read non-privileged hidden information such as timezone,
|
||||
whether the member list is hidden, and whether a member is unlisted.
|
||||
- `user.read_privileged`: can read privileged information such as authentication methods
|
||||
- `user.update`: can update the user's profile.
|
||||
**cannot** update anything locked behind `user.read_privileged`
|
||||
- `member.read`: can view member list if it's hidden and enumerate unlisted members
|
||||
- `member.create`: can create new members
|
||||
- `member.update`: can edit and delete members
|
||||
|
||||
## Meta
|
||||
|
||||
- [x] GET `/meta`: gets stats and server information
|
||||
|
||||
## Users
|
||||
|
||||
- [x] GET `/users/{userRef}`: views current user.
|
||||
`identify` required to use `@me` as user reference.
|
||||
`user.read_hidden` required to view timezone and other hidden non-privileged data.
|
||||
`user.read_privileged` required to view authentication methods.
|
||||
`member.read` required to view unlisted members.
|
||||
- [x] PATCH `/users/@me`: updates current user. `user.update` required
|
||||
- [x] PATCH `/users/@me/custom-preferences`: updates user's custom preferences. `user.update` required
|
||||
- [ ] DELETE `/users/@me`: deletes current user. `*` required
|
||||
- [ ] POST `/users/@me/export`: queues new data export. `*` required
|
||||
- [ ] GET `/users/@me/export`: gets latest data export. `*` required
|
||||
- [ ] GET `/users/@me/flags`: get all the user's flags. `identify` required
|
||||
- [ ] POST `/users/@me/flags`: creates a new flag. `user.update` required
|
||||
- [ ] PATCH `/users/@me/flags/{id}`: updates an existing flag. `user.update` required
|
||||
- [ ] DELETE `/users/@me/flags/{id}`: deletes a user flag. `user.update` required
|
||||
- [ ] POST `/users/@me/reroll`: rerolls a user's short ID. `user.update` required
|
||||
|
||||
## Members
|
||||
|
||||
- [x] GET `/users/{userRef}/members`: gets list of a user's members.
|
||||
if the user's member list is hidden,
|
||||
and it is not the authenticated user (or the token doesn't have the `member.read` scope)
|
||||
returns an empty array.
|
||||
- [x] GET `/users/{userRef}/members/{memberRef}`: gets a single member.
|
||||
will always return a member if it exists, even if the member is unlisted.
|
||||
- [x] POST `/users/@me/members`: creates a new member. `member.create` required
|
||||
- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required
|
||||
- [x] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required
|
||||
- [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required.
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend;
|
||||
|
||||
public static class BuildInfo
|
||||
|
@ -7,20 +21,27 @@ public static class BuildInfo
|
|||
|
||||
public static async Task ReadBuildInfo()
|
||||
{
|
||||
await using var stream = typeof(BuildInfo).Assembly.GetManifestResourceStream("version");
|
||||
if (stream == null) return;
|
||||
await using Stream? stream = typeof(BuildInfo).Assembly.GetManifestResourceStream(
|
||||
"version"
|
||||
);
|
||||
if (stream == null)
|
||||
return;
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var data = (await reader.ReadToEndAsync()).Trim().Split("\n");
|
||||
if (data.Length < 3) return;
|
||||
string[] data = (await reader.ReadToEndAsync()).Trim().Split("\n");
|
||||
if (data.Length < 3)
|
||||
return;
|
||||
|
||||
Hash = data[0];
|
||||
var dirty = data[2] == "dirty";
|
||||
bool dirty = data[2] == "dirty";
|
||||
|
||||
var versionData = data[1].Split("-");
|
||||
if (versionData.Length < 3) return;
|
||||
string[] versionData = data[1].Split("-");
|
||||
if (versionData.Length < 3)
|
||||
return;
|
||||
Version = versionData[0];
|
||||
if (versionData[1] != "0" || dirty) Version += $"+{versionData[2]}";
|
||||
if (dirty) Version += ".dirty";
|
||||
if (versionData[1] != "0" || dirty)
|
||||
Version += $"+{versionData[2]}";
|
||||
if (dirty)
|
||||
Version += ".dirty";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,115 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Foxnouns.Backend;
|
||||
|
||||
public class Config
|
||||
{
|
||||
public string Host { get; set; } = "localhost";
|
||||
public int Port { get; set; } = 3000;
|
||||
public string BaseUrl { get; set; } = null!;
|
||||
public string Host { get; init; } = "localhost";
|
||||
public int Port { get; init; } = 3000;
|
||||
public string BaseUrl { get; init; } = null!;
|
||||
public string MediaBaseUrl { get; init; } = null!;
|
||||
|
||||
public string Address => $"http://{Host}:{Port}";
|
||||
|
||||
public string? SeqLogUrl { get; set; }
|
||||
public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug;
|
||||
public LoggingConfig Logging { get; init; } = new();
|
||||
public DatabaseConfig Database { get; init; } = new();
|
||||
public StorageConfig Storage { get; init; } = new();
|
||||
public LimitsConfig Limits { get; init; } = new();
|
||||
public EmailAuthConfig EmailAuth { get; init; } = new();
|
||||
public DiscordAuthConfig DiscordAuth { get; init; } = new();
|
||||
public GoogleAuthConfig GoogleAuth { get; init; } = new();
|
||||
public TumblrAuthConfig TumblrAuth { get; init; } = new();
|
||||
|
||||
public DatabaseConfig Database { get; set; } = new();
|
||||
public class LoggingConfig
|
||||
{
|
||||
public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug;
|
||||
public string? SeqLogUrl { get; init; }
|
||||
public string? SentryUrl { get; init; }
|
||||
public bool SentryTracing { get; init; } = false;
|
||||
public double SentryTracesSampleRate { get; init; } = 0.0;
|
||||
public bool LogQueries { get; init; } = false;
|
||||
public bool EnableMetrics { get; init; } = false;
|
||||
public ushort MetricsPort { get; init; } = 5001;
|
||||
}
|
||||
|
||||
public class DatabaseConfig
|
||||
{
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public int? Timeout { get; set; }
|
||||
public int? MaxPoolSize { get; set; }
|
||||
public string Url { get; init; } = string.Empty;
|
||||
public bool? EnablePooling { get; init; }
|
||||
public int? Timeout { get; init; }
|
||||
public int? MaxPoolSize { get; init; }
|
||||
public string Redis { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageConfig
|
||||
{
|
||||
public string Endpoint { get; init; } = string.Empty;
|
||||
public string AccessKey { get; init; } = string.Empty;
|
||||
public string SecretKey { get; init; } = string.Empty;
|
||||
public string Bucket { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public class EmailAuthConfig
|
||||
{
|
||||
public bool Enabled => From != null;
|
||||
public string? From { get; init; }
|
||||
}
|
||||
|
||||
public class DiscordAuthConfig
|
||||
{
|
||||
public bool Enabled => ClientId != null && ClientSecret != null;
|
||||
|
||||
public string? ClientId { get; init; }
|
||||
public string? ClientSecret { get; init; }
|
||||
}
|
||||
|
||||
public class GoogleAuthConfig
|
||||
{
|
||||
public bool Enabled => ClientId != null && ClientSecret != null;
|
||||
|
||||
public string? ClientId { get; init; }
|
||||
public string? ClientSecret { get; init; }
|
||||
}
|
||||
|
||||
public class TumblrAuthConfig
|
||||
{
|
||||
public bool Enabled => ClientId != null && ClientSecret != null;
|
||||
|
||||
public string? ClientId { get; init; }
|
||||
public string? ClientSecret { get; init; }
|
||||
}
|
||||
|
||||
public class LimitsConfig
|
||||
{
|
||||
public int MaxMemberCount { get; init; } = 1000;
|
||||
|
||||
public int MaxFields { get; init; } = 25;
|
||||
public int MaxFieldNameLength { get; init; } = 100;
|
||||
public int MaxFieldEntryTextLength { get; init; } = 100;
|
||||
public int MaxFieldEntries { get; init; } = 100;
|
||||
|
||||
public int MaxUsernameLength { get; init; } = 40;
|
||||
public int MaxMemberNameLength { get; init; } = 100;
|
||||
public int MaxDisplayNameLength { get; init; } = 100;
|
||||
public int MaxLinks { get; init; } = 25;
|
||||
public int MaxLinkLength { get; init; } = 256;
|
||||
public int MaxBioLength { get; init; } = 1024;
|
||||
public int MaxAvatarLength { get; init; } = 1_500_000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -10,4 +24,4 @@ public class ApiControllerBase : ControllerBase
|
|||
{
|
||||
internal Token? CurrentToken => HttpContext.GetToken();
|
||||
internal User? CurrentUser => HttpContext.GetUser();
|
||||
}
|
||||
}
|
||||
|
|
155
Foxnouns.Backend/Controllers/Authentication/AuthController.cs
Normal file
155
Foxnouns.Backend/Controllers/Authentication/AuthController.cs
Normal file
|
@ -0,0 +1,155 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class AuthController(
|
||||
Config config,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
ILogger logger
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<AuthController>();
|
||||
|
||||
[HttpPost("urls")]
|
||||
[ProducesResponseType<UrlsResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UrlsAsync(CancellationToken ct = default)
|
||||
{
|
||||
_logger.Debug(
|
||||
"Generating auth URLs for Discord: {Discord}, Google: {Google}, Tumblr: {Tumblr}",
|
||||
config.DiscordAuth.Enabled,
|
||||
config.GoogleAuth.Enabled,
|
||||
config.TumblrAuth.Enabled
|
||||
);
|
||||
string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());
|
||||
string? discord = null;
|
||||
string? google = null;
|
||||
string? tumblr = null;
|
||||
if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null })
|
||||
{
|
||||
discord =
|
||||
"https://discord.com/oauth2/authorize?response_type=code"
|
||||
+ $"&client_id={config.DiscordAuth.ClientId}&scope=identify"
|
||||
+ $"&prompt=none&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}";
|
||||
}
|
||||
|
||||
if (config.GoogleAuth is { ClientId: not null, ClientSecret: not null })
|
||||
{
|
||||
google =
|
||||
"https://accounts.google.com/o/oauth2/auth?response_type=code"
|
||||
+ $"&client_id={config.GoogleAuth.ClientId}"
|
||||
+ $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}"
|
||||
+ $"&prompt=select_account&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}";
|
||||
}
|
||||
|
||||
if (config.TumblrAuth is { ClientId: not null, ClientSecret: not null })
|
||||
{
|
||||
tumblr =
|
||||
"https://www.tumblr.com/oauth2/authorize?response_type=code"
|
||||
+ $"&client_id={config.TumblrAuth.ClientId}"
|
||||
+ $"&scope=basic&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/tumblr")}";
|
||||
}
|
||||
|
||||
return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, google, tumblr));
|
||||
}
|
||||
|
||||
[HttpPost("force-log-out")]
|
||||
[Authorize("identify")]
|
||||
public async Task<IActionResult> ForceLogoutAsync()
|
||||
{
|
||||
_logger.Information("Invalidating all tokens for user {UserId}", CurrentUser!.Id);
|
||||
await db
|
||||
.Tokens.Where(t => t.UserId == CurrentUser.Id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.ManuallyExpired, true));
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("methods/{id}")]
|
||||
[Authorize("*")]
|
||||
[ProducesResponseType<AuthMethodResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAuthMethodAsync(Snowflake id)
|
||||
{
|
||||
AuthMethod? authMethod = await db
|
||||
.AuthMethods.Include(a => a.FediverseApplication)
|
||||
.FirstOrDefaultAsync(a => a.UserId == CurrentUser!.Id && a.Id == id);
|
||||
if (authMethod == null)
|
||||
throw new ApiError.NotFound("No authentication method with that ID found.");
|
||||
|
||||
return Ok(UserRendererService.RenderAuthMethod(authMethod));
|
||||
}
|
||||
|
||||
[HttpDelete("methods/{id}")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> DeleteAuthMethodAsync(Snowflake id)
|
||||
{
|
||||
List<AuthMethod> authMethods = await db
|
||||
.AuthMethods.Where(a => a.UserId == CurrentUser!.Id)
|
||||
.ToListAsync();
|
||||
if (authMethods.Count < 2)
|
||||
{
|
||||
throw new ApiError(
|
||||
"You cannot remove your last authentication method.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.LastAuthMethod
|
||||
);
|
||||
}
|
||||
|
||||
AuthMethod? authMethod = authMethods.FirstOrDefault(a => a.Id == id);
|
||||
if (authMethod == null)
|
||||
throw new ApiError.NotFound("No authentication method with that ID found.");
|
||||
|
||||
_logger.Debug(
|
||||
"Deleting auth method {AuthMethodId} for user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
// If this is the user's last email, we should also clear the user's password.
|
||||
if (
|
||||
authMethod.AuthType == AuthType.Email
|
||||
&& authMethods.Count(a => a.AuthType == AuthType.Email) == 1
|
||||
)
|
||||
{
|
||||
_logger.Debug(
|
||||
"Deleted last email address for user {UserId}, resetting their password",
|
||||
CurrentUser.Id
|
||||
);
|
||||
CurrentUser.Password = null;
|
||||
db.Update(CurrentUser);
|
||||
}
|
||||
|
||||
db.Remove(authMethod);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/discord")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DiscordAuthController(
|
||||
[UsedImplicitly] Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<DiscordAuthController>();
|
||||
|
||||
[HttpPost("callback")]
|
||||
[ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
await keyCacheService.ValidateAuthStateAsync(req.State);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
User? user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id);
|
||||
if (user != null)
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
_logger.Debug(
|
||||
"Discord user {Username} ({Id}) authenticated with no local account",
|
||||
remoteUser.Username,
|
||||
remoteUser.Id
|
||||
);
|
||||
|
||||
string ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"discord:{ticket}",
|
||||
remoteUser,
|
||||
Duration.FromMinutes(20)
|
||||
);
|
||||
|
||||
return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
|
||||
{
|
||||
RemoteAuthService.RemoteUser? remoteUser =
|
||||
await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>(
|
||||
$"discord:{req.Ticket}"
|
||||
);
|
||||
if (remoteUser == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Discord && a.RemoteId == remoteUser.Id
|
||||
)
|
||||
)
|
||||
{
|
||||
_logger.Error(
|
||||
"Discord user {Id} has valid ticket but is already linked to an existing account",
|
||||
remoteUser.Id
|
||||
);
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
}
|
||||
|
||||
User user = await authService.CreateUserWithRemoteAuthAsync(
|
||||
req.Username,
|
||||
AuthType.Discord,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddDiscordAccountAsync()
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
string state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Discord
|
||||
);
|
||||
|
||||
string url =
|
||||
"https://discord.com/oauth2/authorize?response_type=code"
|
||||
+ $"&client_id={config.DiscordAuth.ClientId}&scope=identify"
|
||||
+ $"&prompt=none&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}";
|
||||
|
||||
return Ok(new SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Discord
|
||||
);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Discord,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Discord auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Discord,
|
||||
authMethod.RemoteId,
|
||||
authMethod.RemoteUsername
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequirements()
|
||||
{
|
||||
if (!config.DiscordAuth.Enabled)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Discord authentication is not enabled on this instance."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,374 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/email")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EmailAuthController(
|
||||
[UsedImplicitly] Config config,
|
||||
DatabaseContext db,
|
||||
AuthService authService,
|
||||
MailService mailService,
|
||||
EmailRateLimiter rateLimiter,
|
||||
KeyCacheService keyCacheService,
|
||||
UserRendererService userRenderer,
|
||||
IClock clock,
|
||||
ILogger logger
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<EmailAuthController>();
|
||||
|
||||
[HttpPost("register/init")]
|
||||
public async Task<IActionResult> RegisterInitAsync(
|
||||
[FromBody] EmailRegisterRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
if (!req.Email.Contains('@'))
|
||||
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
|
||||
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null);
|
||||
|
||||
// If there's already a user with that email address, pretend we sent an email but actually ignore it
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(
|
||||
a => a.AuthType == AuthType.Email && a.RemoteId == req.Email,
|
||||
ct
|
||||
)
|
||||
)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
if (IsRateLimited())
|
||||
return NoContent();
|
||||
|
||||
mailService.QueueAccountCreationEmail(req.Email, state);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("callback")]
|
||||
public async Task<IActionResult> CallbackAsync([FromBody] EmailCallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State);
|
||||
if (state is not { ExistingUserId: null })
|
||||
throw new ApiError.BadRequest("Invalid state", "state", req.State);
|
||||
|
||||
string ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20));
|
||||
|
||||
return Ok(new CallbackResponse(false, ticket, state.Email, null, null, null));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> CompleteRegistrationAsync(
|
||||
[FromBody] EmailCompleteRegistrationRequest req
|
||||
)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
string? email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}");
|
||||
if (email == null)
|
||||
throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket);
|
||||
|
||||
User user = await authService.CreateUserWithPasswordAsync(
|
||||
req.Username,
|
||||
email,
|
||||
req.Password
|
||||
);
|
||||
Application frontendApp = await db.GetFrontendApplicationAsync();
|
||||
|
||||
(string? tokenStr, Token? token) = authService.GenerateToken(
|
||||
user,
|
||||
frontendApp,
|
||||
["*"],
|
||||
clock.GetCurrentInstant() + Duration.FromDays(365)
|
||||
);
|
||||
db.Add(token);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}");
|
||||
|
||||
return Ok(
|
||||
new AuthResponse(
|
||||
await userRenderer.RenderUserAsync(user, user, renderMembers: false),
|
||||
tokenStr,
|
||||
token.ExpiresAt
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> LoginAsync(
|
||||
[FromBody] EmailLoginRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
(User? user, AuthService.EmailAuthenticationResult authenticationResult) =
|
||||
await authService.AuthenticateUserAsync(req.Email, req.Password, ct);
|
||||
if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired)
|
||||
throw new NotImplementedException("MFA is not implemented yet");
|
||||
|
||||
Application frontendApp = await db.GetFrontendApplicationAsync(ct);
|
||||
|
||||
_logger.Debug("Logging user {Id} in with email and password", user.Id);
|
||||
|
||||
(string? tokenStr, Token? token) = authService.GenerateToken(
|
||||
user,
|
||||
frontendApp,
|
||||
["*"],
|
||||
clock.GetCurrentInstant() + Duration.FromDays(365)
|
||||
);
|
||||
db.Add(token);
|
||||
|
||||
_logger.Debug("Generated token {TokenId} for {UserId}", token.Id, user.Id);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Ok(
|
||||
new AuthResponse(
|
||||
await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct),
|
||||
tokenStr,
|
||||
token.ExpiresAt
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("change-password")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> UpdatePasswordAsync([FromBody] EmailChangePasswordRequest req)
|
||||
{
|
||||
if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current))
|
||||
throw new ApiError.Forbidden("Invalid password");
|
||||
|
||||
ValidationUtils.Validate([("new", ValidationUtils.ValidatePassword(req.New))]);
|
||||
|
||||
await authService.SetUserPasswordAsync(CurrentUser!, req.New);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("forgot-password")]
|
||||
public async Task<IActionResult> ForgotPasswordAsync([FromBody] EmailForgotPasswordRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
if (!req.Email.Contains('@'))
|
||||
throw new ApiError.BadRequest("Email is invalid", "email", req.Email);
|
||||
|
||||
AuthMethod? authMethod = await db
|
||||
.AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email)
|
||||
.FirstOrDefaultAsync();
|
||||
if (authMethod == null)
|
||||
return NoContent();
|
||||
|
||||
string state = await keyCacheService.GenerateForgotPasswordStateAsync(
|
||||
req.Email,
|
||||
authMethod.UserId
|
||||
);
|
||||
|
||||
if (IsRateLimited())
|
||||
return NoContent();
|
||||
|
||||
mailService.QueueResetPasswordEmail(req.Email, state);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<IActionResult> ResetPasswordAsync([FromBody] EmailResetPasswordRequest req)
|
||||
{
|
||||
ForgotPasswordState? state = await keyCacheService.GetForgotPasswordStateAsync(req.State);
|
||||
if (state == null)
|
||||
throw new ApiError.BadRequest("Unknown state", "state", req.State);
|
||||
|
||||
if (
|
||||
!await db
|
||||
.AuthMethods.Where(m =>
|
||||
m.AuthType == AuthType.Email
|
||||
&& m.RemoteId == state.Email
|
||||
&& m.UserId == state.UserId
|
||||
)
|
||||
.AnyAsync()
|
||||
)
|
||||
{
|
||||
throw new ApiError.BadRequest("Invalid state");
|
||||
}
|
||||
|
||||
ValidationUtils.Validate([("password", ValidationUtils.ValidatePassword(req.Password))]);
|
||||
|
||||
User user = await db.Users.FirstAsync(u => u.Id == state.UserId);
|
||||
await authService.SetUserPasswordAsync(user, req.Password);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
mailService.QueuePasswordChangedEmail(state.Email);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddEmailAddressAsync([FromBody] AddEmailAddressRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
List<AuthMethod> emails = await db
|
||||
.AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email)
|
||||
.ToListAsync();
|
||||
if (emails.Count > AuthUtils.MaxAuthMethodsPerType)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Too many email addresses, maximum of 3 per account.",
|
||||
"email",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (emails.Count != 0)
|
||||
{
|
||||
if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Password))
|
||||
throw new ApiError.Forbidden("Invalid password");
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidationUtils.Validate(
|
||||
[("password", ValidationUtils.ValidatePassword(req.Password))]
|
||||
);
|
||||
await authService.SetUserPasswordAsync(CurrentUser!, req.Password);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
string state = await keyCacheService.GenerateRegisterEmailStateAsync(
|
||||
req.Email,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
bool emailExists = await db
|
||||
.AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email)
|
||||
.AnyAsync();
|
||||
if (emailExists)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
if (IsRateLimited())
|
||||
return NoContent();
|
||||
|
||||
mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddEmailCallbackAsync([FromBody] EmailCallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State);
|
||||
if (state?.ExistingUserId != CurrentUser!.Id)
|
||||
throw new ApiError.BadRequest("Invalid state", "state", req.State);
|
||||
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Email,
|
||||
state.Email
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added email auth {AuthId} for user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Email,
|
||||
authMethod.RemoteId,
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That email address is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public record AddEmailAddressRequest(string Email, string Password);
|
||||
|
||||
private void CheckRequirements()
|
||||
{
|
||||
if (!config.EmailAuth.Enabled)
|
||||
throw new ApiError.BadRequest("Email authentication is not enabled on this instance.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the context's IP address is rate limited from dispatching emails.
|
||||
/// </summary>
|
||||
private bool IsRateLimited()
|
||||
{
|
||||
if (HttpContext.Connection.RemoteIpAddress == null)
|
||||
{
|
||||
_logger.Information(
|
||||
"No remote IP address in HTTP context for email-related request, ignoring as we can't rate limit it"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!rateLimiter.IsLimited(
|
||||
HttpContext.Connection.RemoteIpAddress.ToString(),
|
||||
out Duration retryAfter
|
||||
)
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"IP address cannot send email until {RetryAfter}, ignoring",
|
||||
retryAfter
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/fediverse")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class FediverseAuthController(
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
FediverseAuthService fediverseAuthService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService,
|
||||
KeyCacheService keyCacheService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<FediverseAuthController>();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<SingleUrlResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFediverseUrlAsync(
|
||||
[FromQuery] string instance,
|
||||
[FromQuery(Name = "force-refresh")] bool forceRefresh = false
|
||||
)
|
||||
{
|
||||
if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.'))
|
||||
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
|
||||
|
||||
string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh);
|
||||
return Ok(new SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("callback")]
|
||||
[ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> FediverseCallbackAsync([FromBody] FediverseCallbackRequest req)
|
||||
{
|
||||
FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
FediverseAuthService.FediverseUser remoteUser =
|
||||
await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code, req.State);
|
||||
|
||||
User? user = await authService.AuthenticateUserAsync(
|
||||
AuthType.Fediverse,
|
||||
remoteUser.Id,
|
||||
app
|
||||
);
|
||||
if (user != null)
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
string ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync(
|
||||
$"fediverse:{ticket}",
|
||||
new FediverseTicketData(app.Id, remoteUser),
|
||||
Duration.FromMinutes(20)
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new CallbackResponse(
|
||||
false,
|
||||
ticket,
|
||||
$"@{remoteUser.Username}@{app.Domain}",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType<AuthResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
|
||||
{
|
||||
FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
|
||||
$"fediverse:{req.Ticket}"
|
||||
);
|
||||
if (ticketData == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
|
||||
FediverseApplication? app = await db.FediverseApplications.FindAsync(
|
||||
ticketData.ApplicationId
|
||||
);
|
||||
if (app == null)
|
||||
throw new FoxnounsError("Null application found for ticket");
|
||||
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Fediverse
|
||||
&& a.RemoteId == ticketData.User.Id
|
||||
&& a.FediverseApplicationId == app.Id
|
||||
)
|
||||
)
|
||||
{
|
||||
_logger.Error(
|
||||
"Fediverse user {Id}/{ApplicationId} ({Username} on {Domain}) has valid ticket but is already linked to an existing account",
|
||||
ticketData.User.Id,
|
||||
ticketData.ApplicationId,
|
||||
ticketData.User.Username,
|
||||
app.Domain
|
||||
);
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
}
|
||||
|
||||
User user = await authService.CreateUserWithRemoteAuthAsync(
|
||||
req.Username,
|
||||
AuthType.Fediverse,
|
||||
ticketData.User.Id,
|
||||
ticketData.User.Username,
|
||||
app
|
||||
);
|
||||
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddFediverseAccountAsync(
|
||||
[FromQuery] string instance,
|
||||
[FromQuery(Name = "force-refresh")] bool forceRefresh = false
|
||||
)
|
||||
{
|
||||
if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.'))
|
||||
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
|
||||
|
||||
string state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
instance
|
||||
);
|
||||
|
||||
string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state);
|
||||
return Ok(new SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync(
|
||||
[FromBody] FediverseCallbackRequest req
|
||||
)
|
||||
{
|
||||
FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance);
|
||||
FediverseAuthService.FediverseUser remoteUser =
|
||||
await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code);
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Fediverse,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username,
|
||||
app
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Fediverse auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Fediverse,
|
||||
authMethod.RemoteId,
|
||||
$"{authMethod.RemoteUsername}@{app.Domain}"
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private record FediverseTicketData(
|
||||
Snowflake ApplicationId,
|
||||
FediverseAuthService.FediverseUser User
|
||||
);
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/google")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class GoogleAuthController(
|
||||
[UsedImplicitly] Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<GoogleAuthController>();
|
||||
|
||||
[HttpPost("callback")]
|
||||
[ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
await keyCacheService.ValidateAuthStateAsync(req.State);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
User? user = await authService.AuthenticateUserAsync(AuthType.Google, remoteUser.Id);
|
||||
if (user != null)
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
_logger.Debug(
|
||||
"Google user {Username} ({Id}) authenticated with no local account",
|
||||
remoteUser.Username,
|
||||
remoteUser.Id
|
||||
);
|
||||
|
||||
string ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync($"google:{ticket}", remoteUser, Duration.FromMinutes(20));
|
||||
|
||||
return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
|
||||
{
|
||||
RemoteAuthService.RemoteUser? remoteUser =
|
||||
await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>($"google:{req.Ticket}");
|
||||
if (remoteUser == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Google && a.RemoteId == remoteUser.Id
|
||||
)
|
||||
)
|
||||
{
|
||||
_logger.Error(
|
||||
"Google user {Id} has valid ticket but is already linked to an existing account",
|
||||
remoteUser.Id
|
||||
);
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
}
|
||||
|
||||
User user = await authService.CreateUserWithRemoteAuthAsync(
|
||||
req.Username,
|
||||
AuthType.Google,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddGoogleAccountAsync()
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
string state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Google
|
||||
);
|
||||
|
||||
string url =
|
||||
"https://accounts.google.com/o/oauth2/auth?response_type=code"
|
||||
+ $"&client_id={config.GoogleAuth.ClientId}"
|
||||
+ $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}"
|
||||
+ $"&prompt=select_account&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}";
|
||||
|
||||
return Ok(new SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Google
|
||||
);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Google,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Google auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Google,
|
||||
authMethod.RemoteId,
|
||||
authMethod.RemoteUsername
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequirements()
|
||||
{
|
||||
if (!config.GoogleAuth.Enabled)
|
||||
{
|
||||
throw new ApiError.BadRequest("Google authentication is not enabled on this instance.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Services.Auth;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Authentication;
|
||||
|
||||
[Route("/api/internal/auth/tumblr")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class TumblrAuthController(
|
||||
[UsedImplicitly] Config config,
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
KeyCacheService keyCacheService,
|
||||
AuthService authService,
|
||||
RemoteAuthService remoteAuthService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<TumblrAuthController>();
|
||||
|
||||
[HttpPost("callback")]
|
||||
[ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
await keyCacheService.ValidateAuthStateAsync(req.State);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestTumblrTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
User? user = await authService.AuthenticateUserAsync(AuthType.Tumblr, remoteUser.Id);
|
||||
if (user != null)
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
|
||||
_logger.Debug(
|
||||
"Tumblr user {Username} ({Id}) authenticated with no local account",
|
||||
remoteUser.Username,
|
||||
remoteUser.Id
|
||||
);
|
||||
|
||||
string ticket = AuthUtils.RandomToken();
|
||||
await keyCacheService.SetKeyAsync($"tumblr:{ticket}", remoteUser, Duration.FromMinutes(20));
|
||||
|
||||
return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
|
||||
{
|
||||
RemoteAuthService.RemoteUser? remoteUser =
|
||||
await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>($"tumblr:{req.Ticket}");
|
||||
if (remoteUser == null)
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
if (
|
||||
await db.AuthMethods.AnyAsync(a =>
|
||||
a.AuthType == AuthType.Tumblr && a.RemoteId == remoteUser.Id
|
||||
)
|
||||
)
|
||||
{
|
||||
_logger.Error(
|
||||
"Tumblr user {Id} has valid ticket but is already linked to an existing account",
|
||||
remoteUser.Id
|
||||
);
|
||||
throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket);
|
||||
}
|
||||
|
||||
User user = await authService.CreateUserWithRemoteAuthAsync(
|
||||
req.Username,
|
||||
AuthType.Tumblr,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
|
||||
return Ok(await authService.GenerateUserTokenAsync(user));
|
||||
}
|
||||
|
||||
[HttpGet("add-account")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddTumblrAccountAsync()
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
string state = await remoteAuthService.ValidateAddAccountRequestAsync(
|
||||
CurrentUser!.Id,
|
||||
AuthType.Tumblr
|
||||
);
|
||||
|
||||
string url =
|
||||
"https://www.tumblr.com/oauth2/authorize?response_type=code"
|
||||
+ $"&client_id={config.TumblrAuth.ClientId}"
|
||||
+ $"&scope=basic&state={state}"
|
||||
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/tumblr")}";
|
||||
|
||||
return Ok(new SingleUrlResponse(url));
|
||||
}
|
||||
|
||||
[HttpPost("add-account/callback")]
|
||||
[Authorize("*")]
|
||||
public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req)
|
||||
{
|
||||
CheckRequirements();
|
||||
|
||||
await remoteAuthService.ValidateAddAccountStateAsync(
|
||||
req.State,
|
||||
CurrentUser!.Id,
|
||||
AuthType.Tumblr
|
||||
);
|
||||
|
||||
RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestTumblrTokenAsync(
|
||||
req.Code
|
||||
);
|
||||
try
|
||||
{
|
||||
AuthMethod authMethod = await authService.AddAuthMethodAsync(
|
||||
CurrentUser.Id,
|
||||
AuthType.Tumblr,
|
||||
remoteUser.Id,
|
||||
remoteUser.Username
|
||||
);
|
||||
_logger.Debug(
|
||||
"Added new Tumblr auth method {AuthMethodId} to user {UserId}",
|
||||
authMethod.Id,
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
return Ok(
|
||||
new AddOauthAccountResponse(
|
||||
authMethod.Id,
|
||||
AuthType.Tumblr,
|
||||
authMethod.RemoteId,
|
||||
authMethod.RemoteUsername
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
throw new ApiError(
|
||||
"That account is already linked.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.AccountAlreadyLinked
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequirements()
|
||||
{
|
||||
if (!config.TumblrAuth.Enabled)
|
||||
{
|
||||
throw new ApiError.BadRequest("Tumblr authentication is not enabled on this instance.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/debug")]
|
||||
public class DebugController(DatabaseContext db, AuthService authSvc, IClock clock, ILogger logger) : ApiControllerBase
|
||||
{
|
||||
[HttpPost("users")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))]
|
||||
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest req)
|
||||
{
|
||||
logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email);
|
||||
|
||||
var user = await authSvc.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password);
|
||||
var frontendApp = await db.GetFrontendApplicationAsync();
|
||||
|
||||
var (tokenStr, token) =
|
||||
authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365));
|
||||
db.Add(token);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new AuthResponse(user.Id, user.Username, tokenStr));
|
||||
}
|
||||
|
||||
public record CreateUserRequest(string Username, string Password, string Email);
|
||||
|
||||
public record AuthResponse(Snowflake Id, string Username, string Token);
|
||||
}
|
89
Foxnouns.Backend/Controllers/DeleteUserController.cs
Normal file
89
Foxnouns.Backend/Controllers/DeleteUserController.cs
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/internal/self-delete")]
|
||||
[Authorize("*")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DeleteUserController(DatabaseContext db, IClock clock, ILogger logger)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<DeleteUserController>();
|
||||
|
||||
[HttpPost("delete")]
|
||||
public async Task<IActionResult> DeleteSelfAsync()
|
||||
{
|
||||
_logger.Information(
|
||||
"User {UserId} has requested their account to be deleted",
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = true;
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant();
|
||||
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("force")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> ForceDeleteAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested an early full delete of their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
// This is the easiest way to force delete a user, don't judge me
|
||||
CurrentUser.DeletedAt = clock.GetCurrentInstant() - Duration.FromDays(365);
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("undelete")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> UndeleteSelfAsync()
|
||||
{
|
||||
if (!CurrentUser!.Deleted)
|
||||
throw new ApiError.BadRequest("Your account isn't deleted.");
|
||||
if (CurrentUser!.DeletedBy != null)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Your account has been suspended and can't be reactivated by yourself."
|
||||
);
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"User {UserId} has requested to undelete their account",
|
||||
CurrentUser.Id
|
||||
);
|
||||
|
||||
CurrentUser.Deleted = false;
|
||||
CurrentUser.DeletedAt = null;
|
||||
db.Update(CurrentUser);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
80
Foxnouns.Backend/Controllers/ExportsController.cs
Normal file
80
Foxnouns.Backend/Controllers/ExportsController.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/internal/data-exports")]
|
||||
[Authorize("identify")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ExportsController(ILogger logger, Config config, IClock clock, DatabaseContext db)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private static readonly Duration MinimumTimeBetween = Duration.FromDays(1);
|
||||
private readonly ILogger _logger = logger.ForContext<ExportsController>();
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetDataExportsAsync()
|
||||
{
|
||||
DataExport? export = await db
|
||||
.DataExports.Where(d => d.UserId == CurrentUser!.Id)
|
||||
.OrderByDescending(d => d.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (export == null)
|
||||
return Ok(new DataExportResponse(null, null));
|
||||
|
||||
return Ok(
|
||||
new DataExportResponse(
|
||||
ExportUrl(CurrentUser!.Id, export.Filename),
|
||||
export.Id.Time + DataExport.Expiration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private string ExportUrl(Snowflake userId, string filename) =>
|
||||
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}/data-export.zip";
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> QueueDataExportAsync()
|
||||
{
|
||||
var snowflakeToCheck = Snowflake.FromInstant(
|
||||
clock.GetCurrentInstant() - MinimumTimeBetween
|
||||
);
|
||||
_logger.Debug(
|
||||
"Checking if user {UserId} has data exports newer than {Snowflake}",
|
||||
CurrentUser!.Id,
|
||||
snowflakeToCheck
|
||||
);
|
||||
if (
|
||||
await db.DataExports.AnyAsync(d =>
|
||||
d.UserId == CurrentUser.Id && d.Id > snowflakeToCheck
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new ApiError.BadRequest("You can't request a new data export so soon.");
|
||||
}
|
||||
|
||||
CreateDataExportJob.Enqueue(CurrentUser.Id);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
209
Foxnouns.Backend/Controllers/FlagsController.cs
Normal file
209
Foxnouns.Backend/Controllers/FlagsController.cs
Normal file
|
@ -0,0 +1,209 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/users/@me/flags")]
|
||||
public class FlagsController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
[Authorize("user.read_flags")]
|
||||
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
|
||||
{
|
||||
List<PrideFlag> flags = await db
|
||||
.PrideFlags.Where(f => f.UserId == CurrentUser!.Id)
|
||||
.OrderBy(f => f.Name)
|
||||
.ThenBy(f => f.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Ok(flags.Select(userRenderer.RenderPrideFlag));
|
||||
}
|
||||
|
||||
public const int MaxFlagCount = 500;
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("user.update_flags")]
|
||||
[ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)]
|
||||
public async Task<IActionResult> CreateFlagAsync([FromBody] CreateFlagRequest req)
|
||||
{
|
||||
int flagCount = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).CountAsync();
|
||||
if (flagCount >= MaxFlagCount)
|
||||
throw new ApiError.BadRequest("Maximum number of flags reached");
|
||||
|
||||
ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image));
|
||||
|
||||
var flag = new PrideFlag
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
UserId = CurrentUser!.Id,
|
||||
Name = req.Name,
|
||||
Description = req.Description,
|
||||
};
|
||||
|
||||
db.Add(flag);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image));
|
||||
return Accepted(userRenderer.RenderPrideFlag(flag));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}")]
|
||||
[Authorize("user.create_flags")]
|
||||
[ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req)
|
||||
{
|
||||
ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null));
|
||||
|
||||
PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f =>
|
||||
f.Id == id && f.UserId == CurrentUser!.Id
|
||||
);
|
||||
if (flag == null)
|
||||
throw new ApiError.NotFound("Unknown flag ID, or it's not your flag.");
|
||||
|
||||
if (req.Name != null)
|
||||
flag.Name = req.Name;
|
||||
|
||||
if (req.HasProperty(nameof(req.Description)))
|
||||
flag.Description = req.Description;
|
||||
|
||||
db.Update(flag);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(userRenderer.RenderPrideFlag(flag));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[Authorize("user.update_flags")]
|
||||
public async Task<IActionResult> DeleteFlagAsync(Snowflake id)
|
||||
{
|
||||
PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f =>
|
||||
f.Id == id && f.UserId == CurrentUser!.Id
|
||||
);
|
||||
if (flag == null)
|
||||
throw new ApiError.NotFound("Unknown flag ID, or it's not your flag.");
|
||||
|
||||
db.PrideFlags.Remove(flag);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static List<(string, ValidationError?)> ValidateFlag(
|
||||
string? name,
|
||||
string? description,
|
||||
string? imageData
|
||||
)
|
||||
{
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
switch (name.Length)
|
||||
{
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
"name",
|
||||
ValidationError.LengthError("Name is too short", 1, 100, name.Length)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case > 100:
|
||||
errors.Add(
|
||||
(
|
||||
"name",
|
||||
ValidationError.LengthError("Name is too long", 1, 100, name.Length)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (description != null)
|
||||
{
|
||||
switch (description.Length)
|
||||
{
|
||||
case < 1:
|
||||
errors.Add(
|
||||
(
|
||||
"description",
|
||||
ValidationError.LengthError(
|
||||
"Description is too short",
|
||||
1,
|
||||
100,
|
||||
description.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case > 500:
|
||||
errors.Add(
|
||||
(
|
||||
"description",
|
||||
ValidationError.LengthError(
|
||||
"Description is too long",
|
||||
1,
|
||||
100,
|
||||
description.Length
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageData != null)
|
||||
{
|
||||
switch (imageData.Length)
|
||||
{
|
||||
case 0:
|
||||
errors.Add(
|
||||
(
|
||||
"image",
|
||||
ValidationError.GenericValidationError("Image cannot be empty", null)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case > 1_500_000:
|
||||
errors.Add(
|
||||
(
|
||||
"image",
|
||||
ValidationError.GenericValidationError("Image is too large", null)
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
123
Foxnouns.Backend/Controllers/InternalController.cs
Normal file
123
Foxnouns.Backend/Controllers/InternalController.cs
Normal file
|
@ -0,0 +1,123 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Text.RegularExpressions;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/internal")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public partial class InternalController(DatabaseContext db) : ControllerBase
|
||||
{
|
||||
[GeneratedRegex(@"(\{\w+\})")]
|
||||
private static partial Regex PathVarRegex();
|
||||
|
||||
[GeneratedRegex(@"\{id\}")]
|
||||
private static partial Regex IdCountRegex();
|
||||
|
||||
private static string GetCleanedTemplate(string template)
|
||||
{
|
||||
if (template.StartsWith("api/v2"))
|
||||
template = template["api/v2".Length..];
|
||||
else if (template.StartsWith("api/v1"))
|
||||
template = template["api/v1".Length..];
|
||||
template = PathVarRegex()
|
||||
.Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}`
|
||||
.Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}`
|
||||
|
||||
// If there's at least one path parameter, we only return the *first* part of the path.
|
||||
if (template.Contains("{id}"))
|
||||
{
|
||||
// However, if the path starts with /users/{id} *and* there's another path parameter (such as a member ID)
|
||||
// we ignore the leading /users/{id}. This is because a lot of routes are scoped by user, but should have
|
||||
// separate rate limits from other user-scoped routes.
|
||||
if (template.StartsWith("/users/{id}/") && IdCountRegex().Count(template) >= 2)
|
||||
template = template["/users/{id}".Length..];
|
||||
|
||||
return template.Split("{id}")[0] + "{id}";
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
[HttpPost("request-data")]
|
||||
public async Task<IActionResult> GetRequestDataAsync([FromBody] RequestDataRequest req)
|
||||
{
|
||||
RouteEndpoint? endpoint = GetEndpoint(HttpContext, req.Path, req.Method);
|
||||
if (endpoint == null)
|
||||
throw new ApiError.BadRequest("Path/method combination is invalid");
|
||||
|
||||
ControllerActionDescriptor? actionDescriptor =
|
||||
endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
|
||||
string? template = actionDescriptor?.AttributeRouteInfo?.Template;
|
||||
if (template == null)
|
||||
throw new FoxnounsError("Template value was null on valid endpoint");
|
||||
template = GetCleanedTemplate(template);
|
||||
|
||||
// If no token was supplied, or it isn't valid base 64, return a null user ID (limiting by IP)
|
||||
if (!AuthUtils.TryParseToken(req.Token, out byte[]? rawToken))
|
||||
return Ok(new RequestDataResponse(null, template));
|
||||
|
||||
Snowflake? userId = await db.GetTokenUserId(rawToken);
|
||||
return Ok(new RequestDataResponse(userId, template));
|
||||
}
|
||||
|
||||
private static RouteEndpoint? GetEndpoint(
|
||||
HttpContext httpContext,
|
||||
string url,
|
||||
string requestMethod
|
||||
)
|
||||
{
|
||||
EndpointDataSource? endpointDataSource =
|
||||
httpContext.RequestServices.GetService<EndpointDataSource>();
|
||||
if (endpointDataSource == null)
|
||||
return null;
|
||||
IEnumerable<RouteEndpoint> endpoints = endpointDataSource.Endpoints.OfType<RouteEndpoint>();
|
||||
|
||||
foreach (RouteEndpoint? endpoint in endpoints)
|
||||
{
|
||||
if (endpoint.RoutePattern.RawText == null)
|
||||
continue;
|
||||
|
||||
var templateMatcher = new TemplateMatcher(
|
||||
TemplateParser.Parse(endpoint.RoutePattern.RawText),
|
||||
new RouteValueDictionary()
|
||||
);
|
||||
if (!templateMatcher.TryMatch(url, new RouteValueDictionary()))
|
||||
continue;
|
||||
HttpMethodAttribute? httpMethodAttribute =
|
||||
endpoint.Metadata.GetMetadata<HttpMethodAttribute>();
|
||||
if (
|
||||
httpMethodAttribute?.HttpMethods.Any(x =>
|
||||
x.Equals(requestMethod, StringComparison.OrdinalIgnoreCase)
|
||||
) == false
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
322
Foxnouns.Backend/Controllers/MembersController.cs
Normal file
322
Foxnouns.Backend/Controllers/MembersController.cs
Normal file
|
@ -0,0 +1,322 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using NodaTime;
|
||||
using XidNet;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/users/{userRef}/members")]
|
||||
public class MembersController(
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
MemberRendererService memberRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
ObjectStorageService objectStorageService,
|
||||
IClock clock,
|
||||
ValidationService validationService,
|
||||
Config config
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<MembersController>();
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken));
|
||||
}
|
||||
|
||||
[HttpGet("{memberRef}")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Member member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct);
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v2/users/@me/members")]
|
||||
[ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
|
||||
[Authorize("member.create")]
|
||||
public async Task<IActionResult> CreateMemberAsync(
|
||||
[FromBody] CreateMemberRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate(
|
||||
[
|
||||
("name", validationService.ValidateMemberName(req.Name)),
|
||||
("display_name", validationService.ValidateDisplayName(req.DisplayName)),
|
||||
("bio", validationService.ValidateBio(req.Bio)),
|
||||
("avatar", validationService.ValidateAvatar(req.Avatar)),
|
||||
.. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences),
|
||||
.. validationService.ValidateFieldEntries(
|
||||
req.Names?.ToArray(),
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
),
|
||||
.. validationService.ValidatePronouns(
|
||||
req.Pronouns?.ToArray(),
|
||||
CurrentUser!.CustomPreferences
|
||||
),
|
||||
.. validationService.ValidateLinks(req.Links),
|
||||
]
|
||||
);
|
||||
|
||||
int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct);
|
||||
if (memberCount >= config.Limits.MaxMemberCount)
|
||||
throw new ApiError.BadRequest("Maximum number of members reached");
|
||||
|
||||
var member = new Member
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
LegacyId = Xid.NewXid().ToString(),
|
||||
User = CurrentUser!,
|
||||
Name = req.Name,
|
||||
DisplayName = req.DisplayName,
|
||||
Bio = req.Bio,
|
||||
Links = req.Links ?? [],
|
||||
Fields = req.Fields ?? [],
|
||||
Names = req.Names ?? [],
|
||||
Pronouns = req.Pronouns ?? [],
|
||||
Unlisted = req.Unlisted ?? false,
|
||||
Sid = null!,
|
||||
};
|
||||
db.Add(member);
|
||||
|
||||
_logger.Debug(
|
||||
"Creating member {MemberName} ({Id}) for {UserId}",
|
||||
member.Name,
|
||||
member.Id,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
_logger.Debug("Could not create member {Id} due to name conflict", member.Id);
|
||||
throw new ApiError.BadRequest(
|
||||
"A member with that name already exists",
|
||||
"name",
|
||||
req.Name
|
||||
);
|
||||
}
|
||||
|
||||
if (req.Avatar != null)
|
||||
{
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
}
|
||||
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
}
|
||||
|
||||
[HttpPatch("/api/v2/users/@me/members/{memberRef}")]
|
||||
[ProducesResponseType<MemberResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[Authorize("member.update")]
|
||||
public async Task<IActionResult> UpdateMemberAsync(
|
||||
string memberRef,
|
||||
[FromBody] UpdateMemberRequest req
|
||||
)
|
||||
{
|
||||
await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync();
|
||||
Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
// We might add extra validations for names later down the line.
|
||||
// These should only take effect when a member's name is changed, not on other changes.
|
||||
if (req.Name != null && req.Name != member.Name)
|
||||
{
|
||||
errors.Add(("name", validationService.ValidateMemberName(req.Name)));
|
||||
member.Name = req.Name;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.DisplayName)))
|
||||
{
|
||||
errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName)));
|
||||
member.DisplayName = req.DisplayName;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Bio)))
|
||||
{
|
||||
errors.Add(("bio", validationService.ValidateBio(req.Bio)));
|
||||
member.Bio = req.Bio;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Links)))
|
||||
{
|
||||
errors.AddRange(validationService.ValidateLinks(req.Links));
|
||||
member.Links = req.Links ?? [];
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Unlisted)))
|
||||
member.Unlisted = req.Unlisted ?? false;
|
||||
|
||||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
)
|
||||
);
|
||||
member.Names = req.Names.ToList();
|
||||
}
|
||||
|
||||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
member.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
||||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
);
|
||||
member.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
||||
if (req.Flags != null)
|
||||
{
|
||||
ValidationError? flagError = await db.SetMemberFlagsAsync(
|
||||
CurrentUser!.Id,
|
||||
member.Id,
|
||||
req.Flags
|
||||
);
|
||||
if (flagError != null)
|
||||
errors.Add(("flags", flagError));
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar)));
|
||||
|
||||
ValidationUtils.Validate(errors);
|
||||
// This is fired off regardless of whether the transaction is committed
|
||||
// (atomic operations are hard when combined with background jobs)
|
||||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar));
|
||||
}
|
||||
|
||||
CurrentUser.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(CurrentUser);
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
_logger.Debug(
|
||||
"Could not update member {Id} due to name conflict ({CurrentName} / {NewName})",
|
||||
member.Id,
|
||||
member.Name,
|
||||
req.Name
|
||||
);
|
||||
throw new ApiError.BadRequest(
|
||||
"A member with that name already exists",
|
||||
"name",
|
||||
req.Name
|
||||
);
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken));
|
||||
}
|
||||
|
||||
[HttpDelete("/api/v2/users/@me/members/{memberRef}")]
|
||||
[Authorize("member.update")]
|
||||
public async Task<IActionResult> DeleteMemberAsync(string memberRef)
|
||||
{
|
||||
Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
||||
int deleteCount = await db
|
||||
.Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id)
|
||||
.ExecuteDeleteAsync();
|
||||
if (deleteCount == 0)
|
||||
{
|
||||
_logger.Warning(
|
||||
"Successfully resolved member {Id} but could not delete them",
|
||||
member.Id
|
||||
);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
if (member.Avatar != null)
|
||||
await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")]
|
||||
[Authorize("member.update")]
|
||||
[ProducesResponseType<MemberResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RerollSidAsync(string memberRef)
|
||||
{
|
||||
Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);
|
||||
|
||||
Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1);
|
||||
if (CurrentUser!.LastSidReroll > minTimeAgo)
|
||||
throw new ApiError.BadRequest("Cannot reroll short ID yet");
|
||||
|
||||
// Using ExecuteUpdateAsync here as the new short ID is generated by the database
|
||||
await db
|
||||
.Members.Where(m => m.Id == member.Id)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(m => m.Sid, _ => db.FindFreeMemberSid()));
|
||||
|
||||
await db
|
||||
.Users.Where(u => u.Id == CurrentUser.Id)
|
||||
.ExecuteUpdateAsync(s =>
|
||||
s.SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant())
|
||||
.SetProperty(u => u.LastActive, clock.GetCurrentInstant())
|
||||
);
|
||||
|
||||
// Fetch the new sid then pass that to RenderMember
|
||||
string newSid = await db
|
||||
.Members.Where(m => m.Id == member.Id)
|
||||
.Select(m => m.Sid)
|
||||
.FirstAsync();
|
||||
return Ok(memberRenderer.RenderMember(member, CurrentToken, newSid));
|
||||
}
|
||||
}
|
85
Foxnouns.Backend/Controllers/MetaController.cs
Normal file
85
Foxnouns.Backend/Controllers/MetaController.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Text.RegularExpressions;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Services.Caching;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/meta")]
|
||||
public partial class MetaController(Config config, NoticeCacheService noticeCache)
|
||||
: ApiControllerBase
|
||||
{
|
||||
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetMeta(CancellationToken ct = default) =>
|
||||
Ok(
|
||||
new MetaResponse(
|
||||
Repository,
|
||||
BuildInfo.Version,
|
||||
BuildInfo.Hash,
|
||||
(int)FoxnounsMetrics.MemberCount.Value,
|
||||
new UserInfoResponse(
|
||||
(int)FoxnounsMetrics.UsersCount.Value,
|
||||
(int)FoxnounsMetrics.UsersActiveMonthCount.Value,
|
||||
(int)FoxnounsMetrics.UsersActiveWeekCount.Value,
|
||||
(int)FoxnounsMetrics.UsersActiveDayCount.Value
|
||||
),
|
||||
new LimitsResponse(
|
||||
config.Limits.MaxMemberCount,
|
||||
config.Limits.MaxBioLength,
|
||||
ValidationUtils.MaxCustomPreferences,
|
||||
AuthUtils.MaxAuthMethodsPerType,
|
||||
FlagsController.MaxFlagCount
|
||||
),
|
||||
Notice: NoticeResponse(await noticeCache.GetAsync(ct))
|
||||
)
|
||||
);
|
||||
|
||||
private static MetaNoticeResponse? NoticeResponse(Notice? notice) =>
|
||||
notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message);
|
||||
|
||||
[HttpGet("page/{page}")]
|
||||
public async Task<IActionResult> GetStaticPageAsync(string page, CancellationToken ct = default)
|
||||
{
|
||||
if (!PageRegex().IsMatch(page))
|
||||
{
|
||||
throw new ApiError.BadRequest("Invalid page name");
|
||||
}
|
||||
|
||||
string path = Path.Join(Directory.GetCurrentDirectory(), "static-pages", $"{page}.md");
|
||||
try
|
||||
{
|
||||
string text = await System.IO.File.ReadAllTextAsync(path, ct);
|
||||
return Ok(text);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
throw new ApiError.NotFound("Page not found", code: ErrorCode.PageNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/api/v2/coffee")]
|
||||
public IActionResult BrewCoffee() =>
|
||||
StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!");
|
||||
|
||||
[GeneratedRegex(@"^[a-z\-_]+$")]
|
||||
private static partial Regex PageRegex();
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/moderation/audit-log")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class AuditLogController(DatabaseContext db, ModerationRendererService moderationRenderer)
|
||||
: ApiControllerBase
|
||||
{
|
||||
public async Task<IActionResult> GetAuditLogAsync(
|
||||
[FromQuery] AuditLogEntryType? type = null,
|
||||
[FromQuery] int? limit = null,
|
||||
[FromQuery] Snowflake? before = null,
|
||||
[FromQuery] Snowflake? after = null,
|
||||
[FromQuery(Name = "by-moderator")] Snowflake? byModerator = null
|
||||
)
|
||||
{
|
||||
limit = limit switch
|
||||
{
|
||||
> 100 => 100,
|
||||
< 0 => 100,
|
||||
null => 100,
|
||||
_ => limit,
|
||||
};
|
||||
|
||||
IQueryable<AuditLogEntry> query = db
|
||||
.AuditLog.Include(e => e.Report)
|
||||
.OrderByDescending(e => e.Id);
|
||||
|
||||
if (before != null)
|
||||
query = query.Where(e => e.Id < before.Value);
|
||||
else if (after != null)
|
||||
query = query.Where(e => e.Id > after.Value);
|
||||
|
||||
if (type != null)
|
||||
query = query.Where(e => e.Type == type);
|
||||
if (byModerator != null)
|
||||
query = query.Where(e => e.ModeratorId == byModerator.Value);
|
||||
|
||||
List<AuditLogEntry> entries = await query.Take(limit!.Value).ToListAsync();
|
||||
|
||||
return Ok(entries.Select(moderationRenderer.RenderAuditLogEntry));
|
||||
}
|
||||
|
||||
[HttpGet("moderators")]
|
||||
public async Task<IActionResult> GetModeratorsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var moderators = await db
|
||||
.Users.Where(u =>
|
||||
!u.Deleted && (u.Role == UserRole.Admin || u.Role == UserRole.Moderator)
|
||||
)
|
||||
.Select(u => new { u.Id, u.Username })
|
||||
.OrderBy(u => u.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Ok(moderators);
|
||||
}
|
||||
}
|
96
Foxnouns.Backend/Controllers/Moderation/LookupController.cs
Normal file
96
Foxnouns.Backend/Controllers/Moderation/LookupController.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/moderation/lookup")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class LookupController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ModerationService moderationService,
|
||||
ModerationRendererService moderationRenderer
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> QueryUsersAsync(
|
||||
[FromBody] QueryUsersRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var query = db.Users.Select(u => new { u.Id, u.Username });
|
||||
query = req.Fuzzy
|
||||
? query.Where(u => u.Username.Contains(req.Query))
|
||||
: query.Where(u => u.Username == req.Query);
|
||||
|
||||
var users = await query.OrderBy(u => u.Id).Take(100).ToListAsync(ct);
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> QueryUserAsync(Snowflake id, CancellationToken ct = default)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id, ct);
|
||||
|
||||
bool showSensitiveData = await moderationService.ShowSensitiveDataAsync(
|
||||
CurrentUser!,
|
||||
user,
|
||||
ct
|
||||
);
|
||||
|
||||
List<AuthMethod> authMethods = showSensitiveData
|
||||
? await db
|
||||
.AuthMethods.Where(a => a.UserId == user.Id)
|
||||
.Include(a => a.FediverseApplication)
|
||||
.ToListAsync(ct)
|
||||
: [];
|
||||
|
||||
return Ok(
|
||||
new QueryUserResponse(
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
renderMembers: false,
|
||||
renderAuthMethods: false,
|
||||
ct: ct
|
||||
),
|
||||
MemberListHidden: user.ListHidden,
|
||||
LastActive: user.LastActive,
|
||||
LastSidReroll: user.LastSidReroll,
|
||||
Suspended: user is { Deleted: true, DeletedBy: not null },
|
||||
Deleted: user.Deleted,
|
||||
ShowSensitiveData: showSensitiveData,
|
||||
AuthMethods: showSensitiveData
|
||||
? authMethods.Select(UserRendererService.RenderAuthMethod)
|
||||
: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/sensitive")]
|
||||
public async Task<IActionResult> QuerySensitiveUserDataAsync(
|
||||
Snowflake id,
|
||||
[FromBody] QuerySensitiveUserDataRequest req
|
||||
)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id);
|
||||
|
||||
// Don't let mods accidentally spam the audit log
|
||||
bool alreadyAuthorized = await moderationService.ShowSensitiveDataAsync(CurrentUser!, user);
|
||||
if (alreadyAuthorized)
|
||||
return NoContent();
|
||||
|
||||
AuditLogEntry entry = await moderationService.QuerySensitiveDataAsync(
|
||||
CurrentUser!,
|
||||
user,
|
||||
req.Reason
|
||||
);
|
||||
|
||||
return Ok(moderationRenderer.RenderAuditLogEntry(entry));
|
||||
}
|
||||
}
|
138
Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs
Normal file
138
Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/moderation")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class ModActionsController(
|
||||
DatabaseContext db,
|
||||
ModerationService moderationService,
|
||||
ModerationRendererService moderationRenderer
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpPost("warnings/{id}")]
|
||||
public async Task<IActionResult> WarnUserAsync(Snowflake id, [FromBody] WarnUserRequest req)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id);
|
||||
if (user.Deleted)
|
||||
{
|
||||
throw new ApiError(
|
||||
"This user is already deleted.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidWarningTarget
|
||||
);
|
||||
}
|
||||
|
||||
if (user.Id == CurrentUser!.Id)
|
||||
{
|
||||
throw new ApiError(
|
||||
"You can't warn yourself.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidWarningTarget
|
||||
);
|
||||
}
|
||||
|
||||
Member? member = null;
|
||||
if (req.MemberId != null)
|
||||
{
|
||||
member = await db.Members.FirstOrDefaultAsync(m =>
|
||||
m.Id == req.MemberId && m.UserId == user.Id
|
||||
);
|
||||
if (member == null)
|
||||
throw new ApiError.NotFound("No member with that ID found.");
|
||||
}
|
||||
|
||||
Report? report = null;
|
||||
if (req.ReportId != null)
|
||||
{
|
||||
report = await db.Reports.FindAsync(req.ReportId);
|
||||
if (report is not { Status: ReportStatus.Open })
|
||||
{
|
||||
throw new ApiError.NotFound(
|
||||
"No report with that ID found, or it's already closed."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuditLogEntry entry = await moderationService.ExecuteWarningAsync(
|
||||
CurrentUser,
|
||||
user,
|
||||
member,
|
||||
report,
|
||||
req.Reason,
|
||||
req.ClearFields
|
||||
);
|
||||
|
||||
return Ok(moderationRenderer.RenderAuditLogEntry(entry));
|
||||
}
|
||||
|
||||
[HttpPost("suspensions/{id}")]
|
||||
public async Task<IActionResult> SuspendUserAsync(
|
||||
Snowflake id,
|
||||
[FromBody] SuspendUserRequest req
|
||||
)
|
||||
{
|
||||
User user = await db.ResolveUserAsync(id);
|
||||
if (user.Deleted)
|
||||
{
|
||||
throw new ApiError(
|
||||
"This user is already deleted.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidWarningTarget
|
||||
);
|
||||
}
|
||||
|
||||
if (user.Id == CurrentUser!.Id)
|
||||
{
|
||||
throw new ApiError(
|
||||
"You can't warn yourself.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidWarningTarget
|
||||
);
|
||||
}
|
||||
|
||||
Report? report = null;
|
||||
if (req.ReportId != null)
|
||||
{
|
||||
report = await db.Reports.FindAsync(req.ReportId);
|
||||
if (report is not { Status: ReportStatus.Open })
|
||||
{
|
||||
throw new ApiError.NotFound(
|
||||
"No report with that ID found, or it's already closed."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuditLogEntry entry = await moderationService.ExecuteSuspensionAsync(
|
||||
CurrentUser,
|
||||
user,
|
||||
report,
|
||||
req.Reason,
|
||||
req.ClearProfile
|
||||
);
|
||||
|
||||
return Ok(moderationRenderer.RenderAuditLogEntry(entry));
|
||||
}
|
||||
}
|
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
77
Foxnouns.Backend/Controllers/Moderation/NoticesController.cs
Normal file
|
@ -0,0 +1,77 @@
|
|||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/notices")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public class NoticesController(
|
||||
DatabaseContext db,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetNoticesAsync(CancellationToken ct = default)
|
||||
{
|
||||
List<Notice> notices = await db
|
||||
.Notices.Include(n => n.Author)
|
||||
.OrderByDescending(n => n.Id)
|
||||
.ToListAsync(ct);
|
||||
return Ok(notices.Select(RenderNotice));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateNoticeAsync(CreateNoticeRequest req)
|
||||
{
|
||||
Instant now = clock.GetCurrentInstant();
|
||||
if (req.StartTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"Start time cannot be in the past",
|
||||
"start_time",
|
||||
req.StartTime
|
||||
);
|
||||
}
|
||||
|
||||
if (req.EndTime < now)
|
||||
{
|
||||
throw new ApiError.BadRequest(
|
||||
"End time cannot be in the past",
|
||||
"end_time",
|
||||
req.EndTime
|
||||
);
|
||||
}
|
||||
|
||||
var notice = new Notice
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
Message = req.Message,
|
||||
StartTime = req.StartTime ?? clock.GetCurrentInstant(),
|
||||
EndTime = req.EndTime,
|
||||
Author = CurrentUser!,
|
||||
};
|
||||
|
||||
db.Add(notice);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(RenderNotice(notice));
|
||||
}
|
||||
|
||||
private NoticeResponse RenderNotice(Notice notice) =>
|
||||
new(
|
||||
notice.Id,
|
||||
notice.Message,
|
||||
notice.StartTime,
|
||||
notice.EndTime,
|
||||
userRenderer.RenderPartialUser(notice.Author)
|
||||
);
|
||||
}
|
277
Foxnouns.Backend/Controllers/Moderation/ReportsController.cs
Normal file
277
Foxnouns.Backend/Controllers/Moderation/ReportsController.cs
Normal file
|
@ -0,0 +1,277 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Net;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.Moderation;
|
||||
|
||||
[Route("/api/v2/moderation")]
|
||||
public class ReportsController(
|
||||
ILogger logger,
|
||||
DatabaseContext db,
|
||||
IClock clock,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
UserRendererService userRenderer,
|
||||
MemberRendererService memberRenderer,
|
||||
ModerationRendererService moderationRenderer,
|
||||
ModerationService moderationService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
private readonly ILogger _logger = logger.ForContext<ReportsController>();
|
||||
|
||||
private Snowflake MaxReportId() =>
|
||||
Snowflake.FromInstant(clock.GetCurrentInstant() - Duration.FromHours(12));
|
||||
|
||||
[HttpPost("report-user/{id}")]
|
||||
[Authorize("user.moderation")]
|
||||
public async Task<IActionResult> ReportUserAsync(
|
||||
Snowflake id,
|
||||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
User target = await db.ResolveUserAsync(id);
|
||||
|
||||
if (target.Id == CurrentUser!.Id)
|
||||
{
|
||||
throw new ApiError(
|
||||
"You can't report yourself.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidReportTarget
|
||||
);
|
||||
}
|
||||
|
||||
Snowflake reportCutoff = MaxReportId();
|
||||
if (
|
||||
await db
|
||||
.Reports.Where(r =>
|
||||
r.ReporterId == CurrentUser!.Id
|
||||
&& r.TargetUserId == target.Id
|
||||
&& r.Id > reportCutoff
|
||||
)
|
||||
.AnyAsync()
|
||||
)
|
||||
{
|
||||
_logger.Debug(
|
||||
"User {ReporterId} has already reported {TargetId} in the last 12 hours, ignoring report",
|
||||
CurrentUser!.Id,
|
||||
target.Id
|
||||
);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"Creating report on {TargetId} by {ReporterId}",
|
||||
target.Id,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
string snapshot = JsonConvert.SerializeObject(
|
||||
await userRenderer.RenderUserAsync(target, renderMembers: false)
|
||||
);
|
||||
|
||||
var report = new Report
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
ReporterId = CurrentUser.Id,
|
||||
TargetUserId = target.Id,
|
||||
TargetMemberId = null,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.User,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
||||
db.Reports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("report-member/{id}")]
|
||||
[Authorize("user.moderation")]
|
||||
public async Task<IActionResult> ReportMemberAsync(
|
||||
Snowflake id,
|
||||
[FromBody] CreateReportRequest req
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]);
|
||||
|
||||
Member target = await db.ResolveMemberAsync(id);
|
||||
|
||||
if (target.User.Id == CurrentUser!.Id)
|
||||
{
|
||||
throw new ApiError(
|
||||
"You can't report yourself.",
|
||||
HttpStatusCode.BadRequest,
|
||||
ErrorCode.InvalidReportTarget
|
||||
);
|
||||
}
|
||||
|
||||
Snowflake reportCutoff = MaxReportId();
|
||||
if (
|
||||
await db
|
||||
.Reports.Where(r =>
|
||||
r.ReporterId == CurrentUser!.Id
|
||||
&& r.TargetUserId == target.User.Id
|
||||
&& r.Id > reportCutoff
|
||||
)
|
||||
.AnyAsync()
|
||||
)
|
||||
{
|
||||
_logger.Debug(
|
||||
"User {ReporterId} has already reported {TargetId} in the last 12 hours, ignoring report",
|
||||
CurrentUser!.Id,
|
||||
target.User.Id
|
||||
);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
_logger.Information(
|
||||
"Creating report on {TargetId} (member {TargetMemberId}) by {ReporterId}",
|
||||
target.User.Id,
|
||||
target.Id,
|
||||
CurrentUser!.Id
|
||||
);
|
||||
|
||||
string snapshot = JsonConvert.SerializeObject(memberRenderer.RenderMember(target));
|
||||
|
||||
var report = new Report
|
||||
{
|
||||
Id = snowflakeGenerator.GenerateSnowflake(),
|
||||
ReporterId = CurrentUser.Id,
|
||||
TargetUserId = target.User.Id,
|
||||
TargetMemberId = target.Id,
|
||||
Reason = req.Reason,
|
||||
Context = req.Context,
|
||||
TargetType = ReportTargetType.Member,
|
||||
TargetSnapshot = snapshot,
|
||||
};
|
||||
|
||||
db.Reports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("reports")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public async Task<IActionResult> GetReportsAsync(
|
||||
[FromQuery] int? limit = null,
|
||||
[FromQuery] Snowflake? before = null,
|
||||
[FromQuery] Snowflake? after = null,
|
||||
[FromQuery(Name = "by-reporter")] Snowflake? byReporter = null,
|
||||
[FromQuery(Name = "by-target")] Snowflake? byTarget = null,
|
||||
[FromQuery(Name = "include-closed")] bool includeClosed = false
|
||||
)
|
||||
{
|
||||
limit = limit switch
|
||||
{
|
||||
> 100 => 100,
|
||||
< 0 => 100,
|
||||
null => 100,
|
||||
_ => limit,
|
||||
};
|
||||
|
||||
IQueryable<Report> query = db
|
||||
.Reports.Include(r => r.Reporter)
|
||||
.Include(r => r.TargetUser)
|
||||
.Include(r => r.TargetMember);
|
||||
|
||||
if (byTarget != null && await db.Users.AnyAsync(u => u.Id == byTarget.Value))
|
||||
query = query.Where(r => r.TargetUserId == byTarget.Value);
|
||||
|
||||
if (byReporter != null && await db.Users.AnyAsync(u => u.Id == byReporter.Value))
|
||||
query = query.Where(r => r.ReporterId == byReporter.Value);
|
||||
|
||||
if (before != null)
|
||||
query = query.Where(r => r.Id < before.Value).OrderByDescending(r => r.Id);
|
||||
else if (after != null)
|
||||
query = query.Where(r => r.Id > after.Value).OrderBy(r => r.Id);
|
||||
else
|
||||
query = query.OrderByDescending(r => r.Id);
|
||||
|
||||
if (!includeClosed)
|
||||
query = query.Where(r => r.Status == ReportStatus.Open);
|
||||
|
||||
List<Report> reports = await query.Take(limit!.Value).ToListAsync();
|
||||
|
||||
return Ok(reports.Select(moderationRenderer.RenderReport));
|
||||
}
|
||||
|
||||
[HttpGet("reports/{id}")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public async Task<IActionResult> GetReportAsync(Snowflake id, CancellationToken ct = default)
|
||||
{
|
||||
Report? report = await db
|
||||
.Reports.Include(r => r.Reporter)
|
||||
.Include(r => r.TargetUser)
|
||||
.Include(r => r.TargetMember)
|
||||
.Include(r => r.AuditLogEntry)
|
||||
.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
if (report == null)
|
||||
throw new ApiError.NotFound("No report with that ID found.");
|
||||
|
||||
return Ok(
|
||||
new ReportDetailResponse(
|
||||
Report: moderationRenderer.RenderReport(report),
|
||||
User: await userRenderer.RenderUserAsync(
|
||||
report.TargetUser,
|
||||
renderMembers: false,
|
||||
ct: ct
|
||||
),
|
||||
Member: report.TargetMember != null
|
||||
? memberRenderer.RenderMember(report.TargetMember)
|
||||
: null,
|
||||
AuditLogEntry: report.AuditLogEntry != null
|
||||
? moderationRenderer.RenderAuditLogEntry(report.AuditLogEntry)
|
||||
: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("reports/{id}/ignore")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(RequireModerator = true)]
|
||||
public async Task<IActionResult> IgnoreReportAsync(
|
||||
Snowflake id,
|
||||
[FromBody] IgnoreReportRequest req
|
||||
)
|
||||
{
|
||||
Report? report = await db.Reports.FindAsync(id);
|
||||
if (report == null)
|
||||
throw new ApiError.NotFound("No report with that ID found.");
|
||||
if (report.Status != ReportStatus.Open)
|
||||
throw new ApiError.BadRequest("That report has already been handled.");
|
||||
|
||||
AuditLogEntry entry = await moderationService.IgnoreReportAsync(
|
||||
CurrentUser!,
|
||||
report,
|
||||
req.Reason
|
||||
);
|
||||
|
||||
return Ok(moderationRenderer.RenderAuditLogEntry(entry));
|
||||
}
|
||||
}
|
66
Foxnouns.Backend/Controllers/NotificationsController.cs
Normal file
66
Foxnouns.Backend/Controllers/NotificationsController.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/notifications")]
|
||||
public class NotificationsController(
|
||||
DatabaseContext db,
|
||||
ModerationRendererService moderationRenderer,
|
||||
IClock clock
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetNotificationsAsync([FromQuery] bool all = false)
|
||||
{
|
||||
IQueryable<Notification> query = db.Notifications.Where(n => n.TargetId == CurrentUser!.Id);
|
||||
if (!all)
|
||||
query = query.Where(n => n.AcknowledgedAt == null);
|
||||
|
||||
List<Notification> notifications = await query.OrderByDescending(n => n.Id).ToListAsync();
|
||||
|
||||
return Ok(notifications.Select(moderationRenderer.RenderNotification));
|
||||
}
|
||||
|
||||
[HttpPut("{id}/ack")]
|
||||
[Authorize("user.moderation")]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> AcknowledgeNotificationAsync(Snowflake id)
|
||||
{
|
||||
Notification? notification = await db.Notifications.FirstOrDefaultAsync(n =>
|
||||
n.TargetId == CurrentUser!.Id && n.Id == id
|
||||
);
|
||||
if (notification == null)
|
||||
throw new ApiError.NotFound("Notification not found.");
|
||||
|
||||
if (notification.AcknowledgedAt != null)
|
||||
return Ok(moderationRenderer.RenderNotification(notification));
|
||||
|
||||
notification.AcknowledgedAt = clock.GetCurrentInstant();
|
||||
db.Update(notification);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(moderationRenderer.RenderNotification(notification));
|
||||
}
|
||||
}
|
67
Foxnouns.Backend/Controllers/SidController.cs
Normal file
67
Foxnouns.Backend/Controllers/SidController.cs
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/sid")]
|
||||
[SuppressMessage(
|
||||
"Performance",
|
||||
"CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons",
|
||||
Justification = "Not usable with EFCore"
|
||||
)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class SidController(Config config, DatabaseContext db) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("{**id}")]
|
||||
public async Task<IActionResult> ResolveSidAsync(string id, CancellationToken ct = default) =>
|
||||
id.Length switch
|
||||
{
|
||||
5 => await ResolveUserSidAsync(id, ct),
|
||||
6 => await ResolveMemberSidAsync(id, ct),
|
||||
_ => Redirect(config.BaseUrl),
|
||||
};
|
||||
|
||||
private async Task<IActionResult> ResolveUserSidAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
string? username = await db
|
||||
.Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted)
|
||||
.Select(u => u.Username)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (username == null)
|
||||
return Redirect(config.BaseUrl);
|
||||
|
||||
return Redirect($"{config.BaseUrl}/@{username}");
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ResolveMemberSidAsync(
|
||||
string id,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var member = await db
|
||||
.Members.Include(m => m.User)
|
||||
.Where(m => m.Sid == id.ToLowerInvariant() && !m.User.Deleted)
|
||||
.Select(m => new { m.Name, m.User.Username })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (member == null)
|
||||
return Redirect(config.BaseUrl);
|
||||
|
||||
return Redirect($"{config.BaseUrl}/@{member.Username}/{member.Name}");
|
||||
}
|
||||
}
|
|
@ -1,34 +1,329 @@
|
|||
using System.Diagnostics;
|
||||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using EntityFramework.Exceptions.Common;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto;
|
||||
using Foxnouns.Backend.Jobs;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers;
|
||||
|
||||
[Route("/api/v2/users")]
|
||||
public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase
|
||||
public class UsersController(
|
||||
DatabaseContext db,
|
||||
ILogger logger,
|
||||
UserRendererService userRenderer,
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
IClock clock,
|
||||
ValidationService validationService
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("{userRef}")]
|
||||
public async Task<IActionResult> GetUser(string userRef)
|
||||
{
|
||||
var user = await db.ResolveUserAsync(userRef);
|
||||
return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser));
|
||||
}
|
||||
private readonly ILogger _logger = logger.ForContext<UsersController>();
|
||||
|
||||
[HttpGet("@me")]
|
||||
[Authorize("identify")]
|
||||
public async Task<IActionResult> GetMe()
|
||||
[HttpGet("{userRef}")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
[Limit(UsableByDeletedUsers = true)]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
var user = await db.ResolveUserAsync(CurrentUser!.Id);
|
||||
return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser));
|
||||
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderAuthMethods: true,
|
||||
renderSettings: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPatch("@me")]
|
||||
public Task<IActionResult> UpdateUser([FromBody] UpdateUserRequest req)
|
||||
[Authorize("user.update")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UpdateUserAsync(
|
||||
[FromBody] UpdateUserRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(ct);
|
||||
User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct);
|
||||
var errors = new List<(string, ValidationError?)>();
|
||||
|
||||
if (req.Username != null && req.Username != user.Username)
|
||||
{
|
||||
errors.Add(("username", validationService.ValidateUsername(req.Username)));
|
||||
user.Username = req.Username;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.DisplayName)))
|
||||
{
|
||||
errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName)));
|
||||
user.DisplayName = req.DisplayName;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Bio)))
|
||||
{
|
||||
errors.Add(("bio", validationService.ValidateBio(req.Bio)));
|
||||
user.Bio = req.Bio;
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Links)))
|
||||
{
|
||||
errors.AddRange(validationService.ValidateLinks(req.Links));
|
||||
user.Links = req.Links ?? [];
|
||||
}
|
||||
|
||||
if (req.Names != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFieldEntries(
|
||||
req.Names,
|
||||
CurrentUser!.CustomPreferences,
|
||||
"names"
|
||||
)
|
||||
);
|
||||
user.Names = req.Names.ToList();
|
||||
}
|
||||
|
||||
if (req.Pronouns != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)
|
||||
);
|
||||
user.Pronouns = req.Pronouns.ToList();
|
||||
}
|
||||
|
||||
if (req.Fields != null)
|
||||
{
|
||||
errors.AddRange(
|
||||
validationService.ValidateFields(
|
||||
req.Fields.ToList(),
|
||||
CurrentUser!.CustomPreferences
|
||||
)
|
||||
);
|
||||
user.Fields = req.Fields.ToList();
|
||||
}
|
||||
|
||||
if (req.Flags != null)
|
||||
{
|
||||
ValidationError? flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags);
|
||||
if (flagError != null)
|
||||
errors.Add(("flags", flagError));
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar)));
|
||||
|
||||
if (req.HasProperty(nameof(req.MemberTitle)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(req.MemberTitle))
|
||||
{
|
||||
user.MemberTitle = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(
|
||||
("member_title", validationService.ValidateDisplayName(req.MemberTitle))
|
||||
);
|
||||
user.MemberTitle = req.MemberTitle;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.HasProperty(nameof(req.MemberListHidden)))
|
||||
user.ListHidden = req.MemberListHidden == true;
|
||||
|
||||
if (req.HasProperty(nameof(req.Timezone)))
|
||||
{
|
||||
if (string.IsNullOrEmpty(req.Timezone))
|
||||
{
|
||||
user.Timezone = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (TimeZoneInfo.TryFindSystemTimeZoneById(req.Timezone, out _))
|
||||
{
|
||||
user.Timezone = req.Timezone;
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(
|
||||
(
|
||||
"timezone",
|
||||
ValidationError.GenericValidationError("Invalid timezone", req.Timezone)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValidationUtils.Validate(errors);
|
||||
// This is fired off regardless of whether the transaction is committed
|
||||
// (atomic operations are hard when combined with background jobs)
|
||||
// so it's in a separate block to the validation above.
|
||||
if (req.HasProperty(nameof(req.Avatar)))
|
||||
{
|
||||
UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar));
|
||||
}
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (UniqueConstraintException)
|
||||
{
|
||||
_logger.Debug(
|
||||
"Could not update user {Id} due to name conflict ({CurrentName} / {NewName})",
|
||||
user.Id,
|
||||
user.Username,
|
||||
req.Username
|
||||
);
|
||||
throw new ApiError.BadRequest(
|
||||
"That username is already taken.",
|
||||
"username",
|
||||
req.Username
|
||||
);
|
||||
}
|
||||
|
||||
await tx.CommitAsync(ct);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
renderMembers: false,
|
||||
renderAuthMethods: false,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public record UpdateUserRequest(string? Username, string? DisplayName);
|
||||
}
|
||||
[HttpPatch("@me/custom-preferences")]
|
||||
[Authorize("user.update")]
|
||||
[ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UpdateCustomPreferencesAsync(
|
||||
[FromBody] List<CustomPreferenceUpdateRequest> req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req));
|
||||
|
||||
User user = await db.ResolveUserAsync(CurrentUser!.Id, ct);
|
||||
var preferences = user
|
||||
.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key))
|
||||
.ToDictionary();
|
||||
|
||||
foreach (CustomPreferenceUpdateRequest r in req)
|
||||
{
|
||||
if (r.Id != null && preferences.ContainsKey(r.Id.Value))
|
||||
{
|
||||
preferences[r.Id.Value] = new User.CustomPreference
|
||||
{
|
||||
Favourite = r.Favourite,
|
||||
Icon = r.Icon,
|
||||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = preferences[r.Id.Value].LegacyId,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
preferences[snowflakeGenerator.GenerateSnowflake()] = new User.CustomPreference
|
||||
{
|
||||
Favourite = r.Favourite,
|
||||
Icon = r.Icon,
|
||||
Muted = r.Muted,
|
||||
Size = r.Size,
|
||||
Tooltip = r.Tooltip,
|
||||
LegacyId = Guid.NewGuid(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
user.CustomPreferences = preferences;
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Ok(user.CustomPreferences);
|
||||
}
|
||||
|
||||
[HttpPatch("@me/settings")]
|
||||
[Authorize("user.read_hidden", "user.update")]
|
||||
[ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> UpdateUserSettingsAsync(
|
||||
[FromBody] UpdateUserSettingsRequest req,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct);
|
||||
|
||||
if (req.HasProperty(nameof(req.DarkMode)))
|
||||
user.Settings.DarkMode = req.DarkMode;
|
||||
if (req.HasProperty(nameof(req.LastReadNotice)))
|
||||
user.Settings.LastReadNotice = req.LastReadNotice;
|
||||
|
||||
user.LastActive = clock.GetCurrentInstant();
|
||||
db.Update(user);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Ok(user.Settings);
|
||||
}
|
||||
|
||||
[HttpPost("@me/reroll-sid")]
|
||||
[Authorize("user.update")]
|
||||
[ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> RerollSidAsync()
|
||||
{
|
||||
Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1);
|
||||
if (CurrentUser!.LastSidReroll > minTimeAgo)
|
||||
throw new ApiError.BadRequest("Cannot reroll short ID yet");
|
||||
|
||||
// Using ExecuteUpdateAsync here as the new short ID is generated by the database
|
||||
await db
|
||||
.Users.Where(u => u.Id == CurrentUser.Id)
|
||||
.ExecuteUpdateAsync(s =>
|
||||
s.SetProperty(u => u.Sid, _ => db.FindFreeUserSid())
|
||||
.SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant())
|
||||
.SetProperty(u => u.LastActive, clock.GetCurrentInstant())
|
||||
);
|
||||
|
||||
// Get the user's new sid
|
||||
string newSid = await db
|
||||
.Users.Where(u => u.Id == CurrentUser.Id)
|
||||
.Select(u => u.Sid)
|
||||
.FirstAsync();
|
||||
|
||||
User user = await db.ResolveUserAsync(CurrentUser.Id);
|
||||
return Ok(
|
||||
await userRenderer.RenderUserAsync(
|
||||
user,
|
||||
CurrentUser,
|
||||
CurrentToken,
|
||||
false,
|
||||
overrideSid: newSid
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
120
Foxnouns.Backend/Controllers/V1/V1ReadController.cs
Normal file
120
Foxnouns.Backend/Controllers/V1/V1ReadController.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Dto.V1;
|
||||
using Foxnouns.Backend.Middleware;
|
||||
using Foxnouns.Backend.Services.V1;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Controllers.V1;
|
||||
|
||||
[Route("/api/v1")]
|
||||
public class V1ReadController(
|
||||
UsersV1Service usersV1Service,
|
||||
MembersV1Service membersV1Service,
|
||||
DatabaseContext db
|
||||
) : ApiControllerBase
|
||||
{
|
||||
[HttpGet("users/@me")]
|
||||
[Authorize("identify")]
|
||||
public async Task<IActionResult> GetMeAsync(CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync("@me", CurrentToken, ct);
|
||||
return Ok(await usersV1Service.RenderCurrentUserAsync(user, ct));
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}")]
|
||||
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
return Ok(
|
||||
await usersV1Service.RenderUserAsync(
|
||||
user,
|
||||
CurrentToken,
|
||||
renderMembers: true,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("members/{id}")]
|
||||
public async Task<IActionResult> GetMemberAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(id, ct);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members")]
|
||||
public async Task<IActionResult> GetUserMembersAsync(
|
||||
string userRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct);
|
||||
List<Member> members = await db
|
||||
.Members.Where(m => m.UserId == user.Id)
|
||||
.OrderBy(m => m.Name)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<MemberResponse> responses = [];
|
||||
foreach (Member member in members)
|
||||
{
|
||||
responses.Add(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
user,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(responses);
|
||||
}
|
||||
|
||||
[HttpGet("users/{userRef}/members/{memberRef}")]
|
||||
public async Task<IActionResult> GetUserMemberAsync(
|
||||
string userRef,
|
||||
string memberRef,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Member member = await membersV1Service.ResolveMemberAsync(
|
||||
userRef,
|
||||
memberRef,
|
||||
CurrentToken,
|
||||
ct
|
||||
);
|
||||
return Ok(
|
||||
await membersV1Service.RenderMemberAsync(
|
||||
member,
|
||||
CurrentToken,
|
||||
renderFlags: true,
|
||||
ct: ct
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,20 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public abstract class BaseModel
|
||||
{
|
||||
public required Snowflake Id { get; init; } = SnowflakeGenerator.Instance.GenerateSnowflake();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using EntityFramework.Exceptions.PostgreSQL;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -7,35 +23,57 @@ using Npgsql;
|
|||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public class DatabaseContext : DbContext
|
||||
public class DatabaseContext(DbContextOptions options) : DbContext(options)
|
||||
{
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Member> Members { get; set; }
|
||||
public DbSet<AuthMethod> AuthMethods { get; set; }
|
||||
public DbSet<FediverseApplication> FediverseApplications { get; set; }
|
||||
public DbSet<Token> Tokens { get; set; }
|
||||
public DbSet<Application> Applications { get; set; }
|
||||
|
||||
public DatabaseContext(Config config)
|
||||
{
|
||||
var connString = new NpgsqlConnectionStringBuilder(config.Database.Url)
|
||||
private static string GenerateConnectionString(Config.DatabaseConfig config) =>
|
||||
new NpgsqlConnectionStringBuilder(config.Url)
|
||||
{
|
||||
Timeout = config.Database.Timeout ?? 5,
|
||||
MaxPoolSize = config.Database.MaxPoolSize ?? 50,
|
||||
Pooling = config.EnablePooling ?? true,
|
||||
Timeout = config.Timeout ?? 5,
|
||||
MaxPoolSize = config.MaxPoolSize ?? 50,
|
||||
MinPoolSize = 0,
|
||||
ConnectionPruningInterval = 10,
|
||||
ConnectionIdleLifetime = 10,
|
||||
}.ConnectionString;
|
||||
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString);
|
||||
public static NpgsqlDataSource BuildDataSource(Config config)
|
||||
{
|
||||
var dataSourceBuilder = new NpgsqlDataSourceBuilder(
|
||||
GenerateConnectionString(config.Database)
|
||||
);
|
||||
dataSourceBuilder.UseNodaTime();
|
||||
_dataSource = dataSourceBuilder.Build();
|
||||
dataSourceBuilder.UseJsonNet();
|
||||
return dataSourceBuilder.Build();
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder
|
||||
.ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning))
|
||||
.UseNpgsql(_dataSource, o => o.UseNodaTime())
|
||||
.UseSnakeCaseNamingConvention();
|
||||
public static DbContextOptionsBuilder BuildOptions(
|
||||
DbContextOptionsBuilder options,
|
||||
NpgsqlDataSource dataSource,
|
||||
ILoggerFactory? loggerFactory
|
||||
) =>
|
||||
options
|
||||
.ConfigureWarnings(c => c.Ignore(CoreEventId.SaveChangesFailed))
|
||||
.UseNpgsql(dataSource, o => o.UseNodaTime())
|
||||
.UseLoggerFactory(loggerFactory)
|
||||
.UseSnakeCaseNamingConvention()
|
||||
.UseExceptionProcessor();
|
||||
|
||||
public DbSet<User> Users { get; init; } = null!;
|
||||
public DbSet<Member> Members { get; init; } = null!;
|
||||
public DbSet<AuthMethod> AuthMethods { get; init; } = null!;
|
||||
public DbSet<FediverseApplication> FediverseApplications { get; init; } = null!;
|
||||
public DbSet<Token> Tokens { get; init; } = null!;
|
||||
public DbSet<Application> Applications { get; init; } = null!;
|
||||
public DbSet<DataExport> DataExports { get; init; } = null!;
|
||||
|
||||
public DbSet<PrideFlag> PrideFlags { get; init; } = null!;
|
||||
public DbSet<UserFlag> UserFlags { get; init; } = null!;
|
||||
public DbSet<MemberFlag> MemberFlags { get; init; } = null!;
|
||||
|
||||
public DbSet<Report> Reports { get; init; } = null!;
|
||||
public DbSet<AuditLogEntry> AuditLog { get; init; } = null!;
|
||||
public DbSet<Notification> Notifications { get; init; } = null!;
|
||||
public DbSet<Notice> Notices { get; init; } = null!;
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
{
|
||||
|
@ -46,31 +84,116 @@ public class DatabaseContext : DbContext
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.Username).IsUnique();
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.Sid).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => new { m.UserId, m.Name }).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => m.Sid).IsUnique();
|
||||
modelBuilder.Entity<DataExport>().HasIndex(d => d.Filename).IsUnique();
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.OwnsOne(u => u.Fields, f => f.ToJson())
|
||||
.OwnsOne(u => u.Names, n => n.ToJson())
|
||||
.OwnsOne(u => u.Pronouns, p => p.ToJson());
|
||||
// Two indexes on auth_methods, one for fediverse auth and one for all other types.
|
||||
modelBuilder
|
||||
.Entity<AuthMethod>()
|
||||
.HasIndex(m => new
|
||||
{
|
||||
m.AuthType,
|
||||
m.RemoteId,
|
||||
m.FediverseApplicationId,
|
||||
})
|
||||
.HasFilter("fediverse_application_id IS NOT NULL")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<Member>()
|
||||
.OwnsOne(m => m.Fields, f => f.ToJson())
|
||||
.OwnsOne(m => m.Names, n => n.ToJson())
|
||||
.OwnsOne(m => m.Pronouns, p => p.ToJson());
|
||||
modelBuilder
|
||||
.Entity<AuthMethod>()
|
||||
.HasIndex(m => new { m.AuthType, m.RemoteId })
|
||||
.HasFilter("fediverse_application_id IS NULL")
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder
|
||||
.Entity<AuditLogEntry>()
|
||||
.HasOne(e => e.Report)
|
||||
.WithOne(e => e.AuditLogEntry)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<User>().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()");
|
||||
modelBuilder.Entity<User>().Property(u => u.Fields).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.Names).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.Pronouns).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.CustomPreferences).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<User>().Property(u => u.Settings).HasColumnType("jsonb");
|
||||
|
||||
modelBuilder
|
||||
.Entity<Member>()
|
||||
.Property(m => m.Sid)
|
||||
.HasDefaultValueSql("find_free_member_sid()");
|
||||
modelBuilder.Entity<Member>().Property(m => m.Fields).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Member>().Property(m => m.Names).HasColumnType("jsonb");
|
||||
modelBuilder.Entity<Member>().Property(m => m.Pronouns).HasColumnType("jsonb");
|
||||
|
||||
modelBuilder.Entity<UserFlag>().Navigation(f => f.PrideFlag).AutoInclude();
|
||||
modelBuilder.Entity<MemberFlag>().Navigation(f => f.PrideFlag).AutoInclude();
|
||||
|
||||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!)
|
||||
.HasName("find_free_user_sid");
|
||||
|
||||
modelBuilder
|
||||
.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!)
|
||||
.HasName("find_free_member_sid");
|
||||
|
||||
// Indexes for legacy IDs for APIv1
|
||||
modelBuilder.Entity<User>().HasIndex(u => u.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<Member>().HasIndex(m => m.LegacyId).IsUnique();
|
||||
modelBuilder.Entity<PrideFlag>().HasIndex(f => f.LegacyId).IsUnique();
|
||||
|
||||
// a UUID is not an xid, but this should always be set by the application anyway.
|
||||
// we're just setting it here to shut EFCore up because squashing migrations is for nerds
|
||||
modelBuilder
|
||||
.Entity<User>()
|
||||
.Property(u => u.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<Member>()
|
||||
.Property(m => m.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
modelBuilder
|
||||
.Entity<PrideFlag>()
|
||||
.Property(f => f.LegacyId)
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dummy method that calls <c>find_free_user_sid()</c> when used in an EF Core query.
|
||||
/// </summary>
|
||||
public string FindFreeUserSid() => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// Dummy method that calls <c>find_free_member_sid()</c> when used in an EF Core query.
|
||||
/// </summary>
|
||||
public string FindFreeMemberSid() => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[SuppressMessage(
|
||||
"ReSharper",
|
||||
"UnusedType.Global",
|
||||
Justification = "Used by EF Core's migration generator"
|
||||
)]
|
||||
public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory<DatabaseContext>
|
||||
{
|
||||
public DatabaseContext CreateDbContext(string[] args)
|
||||
{
|
||||
// Read the configuration file
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddConfiguration()
|
||||
.Build()
|
||||
// Get the configuration as our config class
|
||||
.Get<Config>() ?? new();
|
||||
Config config =
|
||||
new ConfigurationBuilder()
|
||||
.AddConfiguration()
|
||||
.Build()
|
||||
// Get the configuration as our config class
|
||||
.Get<Config>() ?? new Config();
|
||||
|
||||
return new DatabaseContext(config);
|
||||
NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config);
|
||||
|
||||
DbContextOptions options = DatabaseContext
|
||||
.BuildOptions(new DbContextOptionsBuilder(), dataSource, null)
|
||||
.Options;
|
||||
|
||||
return new DatabaseContext(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +1,205 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Security.Cryptography;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public static class DatabaseQueryExtensions
|
||||
{
|
||||
public static async Task<User> ResolveUserAsync(this DatabaseContext context, string userRef)
|
||||
public static async Task<User> ResolveUserAsync(
|
||||
this DatabaseContext context,
|
||||
string userRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
User? user;
|
||||
if (Snowflake.TryParse(userRef, out var snowflake))
|
||||
if (userRef == "@me")
|
||||
{
|
||||
user = await context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == snowflake);
|
||||
if (user != null) return user;
|
||||
// Not filtering deleted users, as a suspended user should still be able to look at their own profile.
|
||||
return token != null
|
||||
? await context.Users.FirstAsync(u => u.Id == token.UserId, ct)
|
||||
: throw new ApiError.Unauthorized(
|
||||
"This endpoint requires an authenticated user.",
|
||||
ErrorCode.AuthenticationRequired
|
||||
);
|
||||
}
|
||||
|
||||
user = await context.Users
|
||||
.FirstOrDefaultAsync(u => u.Username == userRef);
|
||||
if (user != null) return user;
|
||||
throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound);
|
||||
User? user;
|
||||
if (Snowflake.TryParse(userRef, out Snowflake? snowflake))
|
||||
{
|
||||
user = await context
|
||||
.Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id))
|
||||
.FirstOrDefaultAsync(u => u.Id == snowflake, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
}
|
||||
|
||||
user = await context
|
||||
.Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id))
|
||||
.FirstOrDefaultAsync(u => u.Username == userRef, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
throw new ApiError.NotFound(
|
||||
"No user with that ID or username found.",
|
||||
ErrorCode.UserNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<User> ResolveUserAsync(this DatabaseContext context, Snowflake id)
|
||||
public static async Task<User> ResolveUserAsync(
|
||||
this DatabaseContext context,
|
||||
Snowflake id,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var user = await context.Users
|
||||
.FirstOrDefaultAsync(u => u.Id == id);
|
||||
if (user != null) return user;
|
||||
throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound);
|
||||
User? user = await context
|
||||
.Users.Where(u => !u.Deleted)
|
||||
.FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
if (user != null)
|
||||
return user;
|
||||
throw new ApiError.NotFound("No user with that ID found.", ErrorCode.UserNotFound);
|
||||
}
|
||||
|
||||
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, Snowflake id)
|
||||
public static async Task<Member> ResolveMemberAsync(
|
||||
this DatabaseContext context,
|
||||
Snowflake id,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
if (member != null) return member;
|
||||
throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound);
|
||||
Member? member = await context
|
||||
.Members.Include(m => m.User)
|
||||
.Where(m => !m.User.Deleted)
|
||||
.FirstOrDefaultAsync(m => m.Id == id, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound);
|
||||
}
|
||||
|
||||
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef)
|
||||
public static async Task<Member> ResolveMemberAsync(
|
||||
this DatabaseContext context,
|
||||
string userRef,
|
||||
string memberRef,
|
||||
Token? token,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var user = await context.ResolveUserAsync(userRef);
|
||||
return await context.ResolveMemberAsync(user.Id, memberRef);
|
||||
User user = await context.ResolveUserAsync(userRef, token, ct);
|
||||
return await context.ResolveMemberAsync(user.Id, memberRef, token, ct);
|
||||
}
|
||||
|
||||
public static async Task<Member> ResolveMemberAsync(this DatabaseContext context, Snowflake userId,
|
||||
string memberRef)
|
||||
public static async Task<Member> ResolveMemberAsync(
|
||||
this DatabaseContext context,
|
||||
Snowflake userId,
|
||||
string memberRef,
|
||||
Token? token = null,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
Member? member;
|
||||
if (Snowflake.TryParse(memberRef, out var snowflake))
|
||||
if (Snowflake.TryParse(memberRef, out Snowflake? snowflake))
|
||||
{
|
||||
member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId);
|
||||
if (member != null) return member;
|
||||
member = await context
|
||||
.Members.Include(m => m.User)
|
||||
.Include(m => m.ProfileFlags)
|
||||
// Return members if their user isn't deleted or the user querying it is the member's owner
|
||||
.Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId))
|
||||
.FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
}
|
||||
|
||||
member = await context.Members
|
||||
.Include(m => m.User)
|
||||
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId);
|
||||
if (member != null) return member;
|
||||
throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound);
|
||||
member = await context
|
||||
.Members.Include(m => m.User)
|
||||
.Include(m => m.ProfileFlags)
|
||||
// Return members if their user isn't deleted or the user querying it is the member's owner
|
||||
.Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId))
|
||||
.FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct);
|
||||
if (member != null)
|
||||
return member;
|
||||
throw new ApiError.NotFound(
|
||||
"No member with that ID or name found.",
|
||||
ErrorCode.MemberNotFound
|
||||
);
|
||||
}
|
||||
|
||||
public static async Task<Application> GetFrontendApplicationAsync(this DatabaseContext context)
|
||||
public static async Task<Application> GetFrontendApplicationAsync(
|
||||
this DatabaseContext context,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0));
|
||||
if (app != null) return app;
|
||||
Application? app = await context.Applications.FirstOrDefaultAsync(
|
||||
a => a.Id == new Snowflake(0),
|
||||
ct
|
||||
);
|
||||
if (app != null)
|
||||
return app;
|
||||
|
||||
app = new Application
|
||||
{
|
||||
Id = new Snowflake(0),
|
||||
ClientId = RandomNumberGenerator.GetHexString(32, true),
|
||||
ClientSecret = OauthUtils.RandomToken(48),
|
||||
ClientSecret = AuthUtils.RandomToken(),
|
||||
Name = "pronouns.cc",
|
||||
Scopes = ["*"],
|
||||
RedirectUris = [],
|
||||
};
|
||||
|
||||
context.Add(app);
|
||||
await context.SaveChangesAsync();
|
||||
await context.SaveChangesAsync(ct);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Token?> GetToken(
|
||||
this DatabaseContext context,
|
||||
byte[] rawToken,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
byte[] hash = SHA512.HashData(rawToken);
|
||||
|
||||
Token? oauthToken = await context
|
||||
.Tokens.Include(t => t.Application)
|
||||
.Include(t => t.User)
|
||||
.FirstOrDefaultAsync(
|
||||
t =>
|
||||
t.Hash == hash
|
||||
&& t.ExpiresAt > SystemClock.Instance.GetCurrentInstant()
|
||||
&& !t.ManuallyExpired,
|
||||
ct
|
||||
);
|
||||
|
||||
return oauthToken;
|
||||
}
|
||||
|
||||
public static async Task<Snowflake?> GetTokenUserId(
|
||||
this DatabaseContext context,
|
||||
byte[] rawToken,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
byte[] hash = SHA512.HashData(rawToken);
|
||||
return await context
|
||||
.Tokens.Where(t =>
|
||||
t.Hash == hash
|
||||
&& t.ExpiresAt > SystemClock.Instance.GetCurrentInstant()
|
||||
&& !t.ManuallyExpired
|
||||
)
|
||||
.Select(t => t.UserId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
}
|
||||
|
|
36
Foxnouns.Backend/Database/DatabaseServiceExtensions.cs
Normal file
36
Foxnouns.Backend/Database/DatabaseServiceExtensions.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Npgsql;
|
||||
using Serilog;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public static class DatabaseServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddFoxnounsDatabase(
|
||||
this IServiceCollection serviceCollection,
|
||||
Config config
|
||||
)
|
||||
{
|
||||
NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config);
|
||||
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(dispose: false);
|
||||
|
||||
serviceCollection.AddDbContext<DatabaseContext>(options =>
|
||||
DatabaseContext.BuildOptions(options, dataSource, loggerFactory)
|
||||
);
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
}
|
96
Foxnouns.Backend/Database/FlagQueryExtensions.cs
Normal file
96
Foxnouns.Backend/Database/FlagQueryExtensions.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
public static class FlagQueryExtensions
|
||||
{
|
||||
private static async Task<List<PrideFlag>> GetFlagsAsync(
|
||||
this DatabaseContext db,
|
||||
Snowflake userId
|
||||
) => await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user's profile flags to the given IDs. Returns a validation error if any of the flag IDs are unknown
|
||||
/// or if too many IDs are given. Duplicates are allowed.
|
||||
/// </summary>
|
||||
public static async Task<ValidationError?> SetUserFlagsAsync(
|
||||
this DatabaseContext db,
|
||||
Snowflake userId,
|
||||
Snowflake[] flagIds
|
||||
)
|
||||
{
|
||||
List<UserFlag> currentFlags = await db
|
||||
.UserFlags.Where(f => f.UserId == userId)
|
||||
.ToListAsync();
|
||||
foreach (UserFlag flag in currentFlags)
|
||||
db.UserFlags.Remove(flag);
|
||||
|
||||
// If there's no new flags to set, we're done
|
||||
if (flagIds.Length == 0)
|
||||
return null;
|
||||
if (flagIds.Length > 100)
|
||||
return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
|
||||
|
||||
List<PrideFlag> flags = await db.GetFlagsAsync(userId);
|
||||
Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray();
|
||||
if (unknownFlagIds.Length != 0)
|
||||
return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds);
|
||||
|
||||
IEnumerable<UserFlag> userFlags = flagIds.Select(id => new UserFlag
|
||||
{
|
||||
PrideFlagId = id,
|
||||
UserId = userId,
|
||||
});
|
||||
db.UserFlags.AddRange(userFlags);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<ValidationError?> SetMemberFlagsAsync(
|
||||
this DatabaseContext db,
|
||||
Snowflake userId,
|
||||
Snowflake memberId,
|
||||
Snowflake[] flagIds
|
||||
)
|
||||
{
|
||||
List<MemberFlag> currentFlags = await db
|
||||
.MemberFlags.Where(f => f.MemberId == memberId)
|
||||
.ToListAsync();
|
||||
foreach (MemberFlag flag in currentFlags)
|
||||
db.MemberFlags.Remove(flag);
|
||||
|
||||
if (flagIds.Length == 0)
|
||||
return null;
|
||||
if (flagIds.Length > 100)
|
||||
return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length);
|
||||
|
||||
List<PrideFlag> flags = await db.GetFlagsAsync(userId);
|
||||
Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray();
|
||||
if (unknownFlagIds.Length != 0)
|
||||
return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds);
|
||||
|
||||
IEnumerable<MemberFlag> memberFlags = flagIds.Select(id => new MemberFlag
|
||||
{
|
||||
PrideFlagId = id,
|
||||
MemberId = memberId,
|
||||
});
|
||||
db.MemberFlags.AddRange(memberFlags);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
@ -5,4 +19,4 @@ namespace Foxnouns.Backend.Database;
|
|||
public interface ISnowflakeGenerator
|
||||
{
|
||||
Snowflake GenerateSnowflake(Instant? time = null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,412 +0,0 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240527132444_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
@ -6,6 +7,8 @@ using NodaTime;
|
|||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240527132444_Init")]
|
||||
public partial class Init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -19,12 +22,10 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
domain = table.Column<string>(type: "text", nullable: false),
|
||||
client_id = table.Column<string>(type: "text", nullable: false),
|
||||
client_secret = table.Column<string>(type: "text", nullable: false),
|
||||
instance_type = table.Column<int>(type: "integer", nullable: false)
|
||||
instance_type = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_fediverse_applications", x => x.id);
|
||||
});
|
||||
constraints: table => table.PrimaryKey("pk_fediverse_applications", x => x.id)
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "users",
|
||||
|
@ -40,12 +41,10 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
role = table.Column<int>(type: "integer", nullable: false),
|
||||
fields = table.Column<string>(type: "jsonb", nullable: false),
|
||||
names = table.Column<string>(type: "jsonb", nullable: false),
|
||||
pronouns = table.Column<string>(type: "jsonb", nullable: false)
|
||||
pronouns = table.Column<string>(type: "jsonb", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_users", x => x.id);
|
||||
});
|
||||
constraints: table => table.PrimaryKey("pk_users", x => x.id)
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "auth_methods",
|
||||
|
@ -56,7 +55,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
remote_id = table.Column<string>(type: "text", nullable: false),
|
||||
remote_username = table.Column<string>(type: "text", nullable: true),
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
fediverse_application_id = table.Column<long>(type: "bigint", nullable: true)
|
||||
fediverse_application_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -65,14 +64,17 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
name: "fk_auth_methods_fediverse_applications_fediverse_application_id",
|
||||
column: x => x.fediverse_application_id,
|
||||
principalTable: "fediverse_applications",
|
||||
principalColumn: "id");
|
||||
principalColumn: "id"
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "fk_auth_methods_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "members",
|
||||
|
@ -88,7 +90,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
fields = table.Column<string>(type: "jsonb", nullable: false),
|
||||
names = table.Column<string>(type: "jsonb", nullable: false),
|
||||
pronouns = table.Column<string>(type: "jsonb", nullable: false)
|
||||
pronouns = table.Column<string>(type: "jsonb", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -98,18 +100,23 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tokens",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
expires_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
expires_at = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
scopes = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
manually_expired = table.Column<bool>(type: "boolean", nullable: false),
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false)
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
|
@ -119,53 +126,56 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_fediverse_application_id",
|
||||
table: "auth_methods",
|
||||
column: "fediverse_application_id");
|
||||
column: "fediverse_application_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_user_id",
|
||||
table: "auth_methods",
|
||||
column: "user_id");
|
||||
column: "user_id"
|
||||
);
|
||||
|
||||
// EF Core doesn't support creating indexes on arbitrary expressions, so we have to create it manually.
|
||||
// Due to historical reasons (I made a mistake while writing the initial migration for the Go version)
|
||||
// only members have case-insensitive names.
|
||||
migrationBuilder.Sql("CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))");
|
||||
migrationBuilder.Sql(
|
||||
"CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tokens_user_id",
|
||||
table: "tokens",
|
||||
column: "user_id");
|
||||
column: "user_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_username",
|
||||
table: "users",
|
||||
column: "username",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "auth_methods");
|
||||
migrationBuilder.DropTable(name: "auth_methods");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "members");
|
||||
migrationBuilder.DropTable(name: "members");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "tokens");
|
||||
migrationBuilder.DropTable(name: "tokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "fediverse_applications");
|
||||
migrationBuilder.DropTable(name: "fediverse_applications");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "users");
|
||||
migrationBuilder.DropTable(name: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,470 +0,0 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240528125310_AddApplications")]
|
||||
partial class AddApplications
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240528125310_AddApplications")]
|
||||
public partial class AddApplications : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -15,14 +18,16 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
table: "tokens",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
defaultValue: 0L
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "hash",
|
||||
table: "tokens",
|
||||
type: "bytea",
|
||||
nullable: false,
|
||||
defaultValue: new byte[0]);
|
||||
defaultValue: Array.Empty<byte>()
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "applications",
|
||||
|
@ -33,17 +38,16 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
client_secret = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: false),
|
||||
scopes = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
redirect_uris = table.Column<string[]>(type: "text[]", nullable: false)
|
||||
redirect_uris = table.Column<string[]>(type: "text[]", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_applications", x => x.id);
|
||||
});
|
||||
constraints: table => table.PrimaryKey("pk_applications", x => x.id)
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_tokens_application_id",
|
||||
table: "tokens",
|
||||
column: "application_id");
|
||||
column: "application_id"
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_tokens_applications_application_id",
|
||||
|
@ -51,7 +55,8 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
column: "application_id",
|
||||
principalTable: "applications",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -59,22 +64,16 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_tokens_applications_application_id",
|
||||
table: "tokens");
|
||||
table: "tokens"
|
||||
);
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "applications");
|
||||
migrationBuilder.DropTable(name: "applications");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_tokens_application_id",
|
||||
table: "tokens");
|
||||
migrationBuilder.DropIndex(name: "ix_tokens_application_id", table: "tokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "application_id",
|
||||
table: "tokens");
|
||||
migrationBuilder.DropColumn(name: "application_id", table: "tokens");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "hash",
|
||||
table: "tokens");
|
||||
migrationBuilder.DropColumn(name: "hash", table: "tokens");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,474 +0,0 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240528145744_AddListHidden")]
|
||||
partial class AddListHidden
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240528145744_AddListHidden")]
|
||||
public partial class AddListHidden : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -15,15 +18,14 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
table: "users",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
defaultValue: false
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "list_hidden",
|
||||
table: "users");
|
||||
migrationBuilder.DropColumn(name: "list_hidden", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,478 +0,0 @@
|
|||
// <auto-generated />
|
||||
using Foxnouns.Backend.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240604142522_AddPassword")]
|
||||
partial class AddPassword
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("Members");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240604142522_AddPassword")]
|
||||
public partial class AddPassword : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -14,15 +17,14 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
name: "password",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "password",
|
||||
table: "users");
|
||||
migrationBuilder.DropColumn(name: "password", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240611225328_AddTemporaryKeyCache")]
|
||||
public partial class AddTemporaryKeyCache : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "temporary_keys",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
key = table.Column<string>(type: "text", nullable: false),
|
||||
value = table.Column<string>(type: "text", nullable: false),
|
||||
expires = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table => table.PrimaryKey("pk_temporary_keys", x => x.id)
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_temporary_keys_key",
|
||||
table: "temporary_keys",
|
||||
column: "key",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "temporary_keys");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240712233806_AddUserLastActive")]
|
||||
public partial class AddUserLastActive : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "last_active",
|
||||
table: "users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValueSql: "now()"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "last_active", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240713000719_AddDeleted")]
|
||||
public partial class AddDeleted : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deleted",
|
||||
table: "users",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "deleted_at",
|
||||
table: "users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "deleted_by",
|
||||
table: "users",
|
||||
type: "bigint",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "deleted", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "deleted_at", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "deleted_by", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240821210355_AddCustomPreferences")]
|
||||
public partial class AddCustomPreferences : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Dictionary<Guid, User.CustomPreference>>(
|
||||
name: "custom_preferences",
|
||||
table: "users",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "custom_preferences", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240905191709_AddUserSettings")]
|
||||
public partial class AddUserSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<UserSettings>(
|
||||
name: "settings",
|
||||
table: "users",
|
||||
type: "jsonb",
|
||||
nullable: false,
|
||||
defaultValueSql: "'{}'"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "settings", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
102
Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs
Normal file
102
Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs
Normal file
|
@ -0,0 +1,102 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240926124950_AddSids")]
|
||||
public partial class AddSids : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "sid",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "last_sid_reroll",
|
||||
table: "users",
|
||||
type: "timestamp with time zone",
|
||||
nullable: false,
|
||||
defaultValueSql: "now() - '1 hour'::interval"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "sid",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_sid",
|
||||
table: "users",
|
||||
column: "sid",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_members_sid",
|
||||
table: "members",
|
||||
column: "sid",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.Sql(
|
||||
@"create function generate_sid(len int) returns text as $$
|
||||
select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, len)
|
||||
$$ language sql volatile;
|
||||
"
|
||||
);
|
||||
migrationBuilder.Sql(
|
||||
@"create function find_free_user_sid() returns text as $$
|
||||
declare new_sid text;
|
||||
begin
|
||||
loop
|
||||
new_sid := generate_sid(5);
|
||||
if not exists (select 1 from users where sid = new_sid) then return new_sid; end if;
|
||||
end loop;
|
||||
end
|
||||
$$ language plpgsql volatile;
|
||||
"
|
||||
);
|
||||
migrationBuilder.Sql(
|
||||
@"create function find_free_member_sid() returns text as $$
|
||||
declare new_sid text;
|
||||
begin
|
||||
loop
|
||||
new_sid := generate_sid(6);
|
||||
if not exists (select 1 from members where sid = new_sid) then return new_sid; end if;
|
||||
end loop;
|
||||
end
|
||||
$$ language plpgsql volatile;"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("drop function find_free_member_sid;");
|
||||
migrationBuilder.Sql("drop function find_free_user_sid;");
|
||||
migrationBuilder.Sql("drop function generate_sid;");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_users_sid", table: "users");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_members_sid", table: "members");
|
||||
|
||||
migrationBuilder.DropColumn(name: "sid", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "last_sid_reroll", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "sid", table: "members");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240926130208_NonNullableSids")]
|
||||
public partial class NonNullableSids : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "sid",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "find_free_user_sid()",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "sid",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "find_free_member_sid()",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "sid",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldDefaultValueSql: "find_free_user_sid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "sid",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldDefaultValueSql: "find_free_member_sid()"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
147
Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs
Normal file
147
Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs
Normal file
|
@ -0,0 +1,147 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20240926180037_AddFlags")]
|
||||
public partial class AddFlags : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "pride_flags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
hash = table.Column<string>(type: "text", nullable: false),
|
||||
name = table.Column<string>(type: "text", nullable: false),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_pride_flags", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_pride_flags_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "member_flags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
member_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
pride_flag_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_member_flags", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_member_flags_members_member_id",
|
||||
column: x => x.member_id,
|
||||
principalTable: "members",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "fk_member_flags_pride_flags_pride_flag_id",
|
||||
column: x => x.pride_flag_id,
|
||||
principalTable: "pride_flags",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "user_flags",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
pride_flag_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_user_flags", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_user_flags_pride_flags_pride_flag_id",
|
||||
column: x => x.pride_flag_id,
|
||||
principalTable: "pride_flags",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "fk_user_flags_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_member_flags_member_id",
|
||||
table: "member_flags",
|
||||
column: "member_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_member_flags_pride_flag_id",
|
||||
table: "member_flags",
|
||||
column: "pride_flag_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_pride_flags_user_id",
|
||||
table: "pride_flags",
|
||||
column: "user_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_flags_pride_flag_id",
|
||||
table: "user_flags",
|
||||
column: "pride_flag_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_flags_user_id",
|
||||
table: "user_flags",
|
||||
column: "user_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "member_flags");
|
||||
|
||||
migrationBuilder.DropTable(name: "user_flags");
|
||||
|
||||
migrationBuilder.DropTable(name: "pride_flags");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241006125003_AddFediverseAccessTokens")]
|
||||
public partial class AddFediverseAccessTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "access_token",
|
||||
table: "fediverse_applications",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "token_valid_until",
|
||||
table: "fediverse_applications",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications");
|
||||
|
||||
migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241123210306_RemoveFediverseApplicationTokens")]
|
||||
public partial class RemoveFediverseApplicationTokens : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications");
|
||||
|
||||
migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "access_token",
|
||||
table: "fediverse_applications",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<Instant>(
|
||||
name: "token_valid_until",
|
||||
table: "fediverse_applications",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241124201309_AddUserTimezone")]
|
||||
public partial class AddUserTimezone : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "timezone",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "timezone", table: "users");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241128202508_AddAuthMethodUniqueIndex")]
|
||||
public partial class AddAuthMethodUniqueIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_auth_type_remote_id",
|
||||
table: "auth_methods",
|
||||
columns: new[] { "auth_type", "remote_id" },
|
||||
unique: true,
|
||||
filter: "fediverse_application_id IS NULL"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id",
|
||||
table: "auth_methods",
|
||||
columns: new[] { "auth_type", "remote_id", "fediverse_application_id" },
|
||||
unique: true,
|
||||
filter: "fediverse_application_id IS NOT NULL"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_auth_methods_auth_type_remote_id",
|
||||
table: "auth_methods"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id",
|
||||
table: "auth_methods"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241202153736_AddDataExports")]
|
||||
public partial class AddDataExports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "data_exports",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
filename = table.Column<string>(type: "text", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_data_exports", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_data_exports_users_user_id",
|
||||
column: x => x.user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_data_exports_filename",
|
||||
table: "data_exports",
|
||||
column: "filename",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_data_exports_user_id",
|
||||
table: "data_exports",
|
||||
column: "user_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "data_exports");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241209134148_NullableFlagHash")]
|
||||
public partial class NullableFlagHash : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "hash",
|
||||
table: "pride_flags",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "hash",
|
||||
table: "pride_flags",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241211193653_AddSentEmailCache")]
|
||||
public partial class AddSentEmailCache : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "sent_emails",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
email = table.Column<string>(type: "text", nullable: false),
|
||||
sent_at = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_sent_emails", x => x.id);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_sent_emails_email_sent_at",
|
||||
table: "sent_emails",
|
||||
columns: new[] { "email", "sent_at" }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "sent_emails");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241217010207_AddReports")]
|
||||
public partial class AddReports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase().Annotation("Npgsql:PostgresExtension:hstore", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notifications",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
target_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
message = table.Column<string>(type: "text", nullable: true),
|
||||
localization_key = table.Column<string>(type: "text", nullable: true),
|
||||
localization_params = table.Column<Dictionary<string, string>>(
|
||||
type: "hstore",
|
||||
nullable: false
|
||||
),
|
||||
acknowledged_at = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notifications", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notifications_users_target_id",
|
||||
column: x => x.target_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "reports",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
reporter_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
target_user_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
target_member_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
reason = table.Column<int>(type: "integer", nullable: false),
|
||||
target_type = table.Column<int>(type: "integer", nullable: false),
|
||||
target_snapshot = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_reports", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_members_target_member_id",
|
||||
column: x => x.target_member_id,
|
||||
principalTable: "members",
|
||||
principalColumn: "id"
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_users_reporter_id",
|
||||
column: x => x.reporter_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "fk_reports_users_target_user_id",
|
||||
column: x => x.target_user_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "audit_log",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
moderator_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
moderator_username = table.Column<string>(type: "text", nullable: false),
|
||||
target_user_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
target_username = table.Column<string>(type: "text", nullable: true),
|
||||
target_member_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
target_member_name = table.Column<string>(type: "text", nullable: true),
|
||||
report_id = table.Column<long>(type: "bigint", nullable: true),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
reason = table.Column<string>(type: "text", nullable: true),
|
||||
cleared_fields = table.Column<string[]>(type: "text[]", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_audit_log", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
column: x => x.report_id,
|
||||
principalTable: "reports",
|
||||
principalColumn: "id"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notifications_target_id",
|
||||
table: "notifications",
|
||||
column: "target_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_reporter_id",
|
||||
table: "reports",
|
||||
column: "reporter_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_target_member_id",
|
||||
table: "reports",
|
||||
column: "target_member_id"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_reports_target_user_id",
|
||||
table: "reports",
|
||||
column: "target_user_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "audit_log");
|
||||
|
||||
migrationBuilder.DropTable(name: "notifications");
|
||||
|
||||
migrationBuilder.DropTable(name: "reports");
|
||||
|
||||
migrationBuilder.AlterDatabase().OldAnnotation("Npgsql:PostgresExtension:hstore", ",,");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241217195351_AddFediAppForceRefresh")]
|
||||
public partial class AddFediAppForceRefresh : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<Dictionary<string, string>>(
|
||||
name: "localization_params",
|
||||
table: "notifications",
|
||||
type: "hstore",
|
||||
nullable: false,
|
||||
oldClrType: typeof(Dictionary<string, string>),
|
||||
oldType: "hstore",
|
||||
oldNullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "force_refresh",
|
||||
table: "fediverse_applications",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "force_refresh", table: "fediverse_applications");
|
||||
|
||||
migrationBuilder.AlterColumn<Dictionary<string, string>>(
|
||||
name: "localization_params",
|
||||
table: "notifications",
|
||||
type: "hstore",
|
||||
nullable: true,
|
||||
oldClrType: typeof(Dictionary<string, string>),
|
||||
oldType: "hstore"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218195457_AddContextToReports")]
|
||||
public partial class AddContextToReports : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "context",
|
||||
table: "reports",
|
||||
type: "text",
|
||||
nullable: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(name: "context", table: "reports");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241218201855_MakeAuditLogReportsNullable")]
|
||||
public partial class MakeAuditLogReportsNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_audit_log_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id"
|
||||
);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_audit_log_reports_report_id",
|
||||
table: "audit_log",
|
||||
column: "report_id",
|
||||
principalTable: "reports",
|
||||
principalColumn: "id"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20241225155818_AddLegacyIds")]
|
||||
public partial class AddLegacyIds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "users",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "pride_flags",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "legacy_id",
|
||||
table: "members",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValueSql: "gen_random_uuid()"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_users_legacy_id",
|
||||
table: "users",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_pride_flags_legacy_id",
|
||||
table: "pride_flags",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_members_legacy_id",
|
||||
table: "members",
|
||||
column: "legacy_id",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(name: "ix_users_legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_pride_flags_legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropIndex(name: "ix_members_legacy_id", table: "members");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "pride_flags");
|
||||
|
||||
migrationBuilder.DropColumn(name: "legacy_id", table: "members");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250304155708_RemoveTemporaryKeys")]
|
||||
public partial class RemoveTemporaryKeys : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "temporary_keys");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "temporary_keys",
|
||||
columns: table => new
|
||||
{
|
||||
id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
expires = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
key = table.Column<string>(type: "text", nullable: false),
|
||||
value = table.Column<string>(type: "text", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_temporary_keys", x => x.id);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_temporary_keys_key",
|
||||
table: "temporary_keys",
|
||||
column: "key",
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
915
Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs
generated
Normal file
|
@ -0,0 +1,915 @@
|
|||
// <auto-generated />
|
||||
using System.Collections.Generic;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DatabaseContext))]
|
||||
[Migration("20250329131053_AddNotices")]
|
||||
partial class AddNotices
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.PrimitiveCollection<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_applications");
|
||||
|
||||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ClearedFields")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("cleared_fields");
|
||||
|
||||
b.Property<long>("ModeratorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("moderator_id");
|
||||
|
||||
b.Property<string>("ModeratorUsername")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("moderator_username");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("ReportId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("report_id");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetMemberName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_member_name");
|
||||
|
||||
b.Property<long?>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.Property<string>("TargetUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_username");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("AuthType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("auth_type");
|
||||
|
||||
b.Property<long?>("FediverseApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("fediverse_application_id");
|
||||
|
||||
b.Property<string>("RemoteId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_id");
|
||||
|
||||
b.Property<string>("RemoteUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("remote_username");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_auth_methods");
|
||||
|
||||
b.HasIndex("FediverseApplicationId")
|
||||
.HasDatabaseName("ix_auth_methods_fediverse_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id")
|
||||
.HasFilter("fediverse_application_id IS NULL");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
|
||||
.HasFilter("fediverse_application_id IS NOT NULL");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("filename");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_data_exports");
|
||||
|
||||
b.HasIndex("Filename")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_data_exports_filename");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_data_exports_user_id");
|
||||
|
||||
b.ToTable("data_exports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ClientId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_id");
|
||||
|
||||
b.Property<string>("ClientSecret")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("client_secret");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<bool>("ForceRefresh")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("force_refresh");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_fediverse_applications");
|
||||
|
||||
b.ToTable("fediverse_applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_member_sid()");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
||||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("MemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("member_id");
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_member_flags");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("ix_member_flags_member_id");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_member_flags_pride_flag_id");
|
||||
|
||||
b.ToTable("member_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("author_id");
|
||||
|
||||
b.Property<Instant>("EndTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("end_time");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Instant>("StartTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("start_time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notices");
|
||||
|
||||
b.HasIndex("AuthorId")
|
||||
.HasDatabaseName("ix_notices_author_id");
|
||||
|
||||
b.ToTable("notices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant?>("AcknowledgedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("acknowledged_at");
|
||||
|
||||
b.Property<string>("LocalizationKey")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("localization_key");
|
||||
|
||||
b.Property<Dictionary<string, string>>("LocalizationParams")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore")
|
||||
.HasColumnName("localization_params");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifications");
|
||||
|
||||
b.HasIndex("TargetId")
|
||||
.HasDatabaseName("ix_notifications_target_id");
|
||||
|
||||
b.ToTable("notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pride_flags");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_pride_flags_legacy_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_pride_flags_user_id");
|
||||
|
||||
b.ToTable("pride_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long>("ReporterId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("reporter_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetSnapshot")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_snapshot");
|
||||
|
||||
b.Property<int>("TargetType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("target_type");
|
||||
|
||||
b.Property<long>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_reports");
|
||||
|
||||
b.HasIndex("ReporterId")
|
||||
.HasDatabaseName("ix_reports_reporter_id");
|
||||
|
||||
b.HasIndex("TargetMemberId")
|
||||
.HasDatabaseName("ix_reports_target_member_id");
|
||||
|
||||
b.HasIndex("TargetUserId")
|
||||
.HasDatabaseName("ix_reports_target_user_id");
|
||||
|
||||
b.ToTable("reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("ApplicationId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("application_id");
|
||||
|
||||
b.Property<Instant>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("ManuallyExpired")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tokens");
|
||||
|
||||
b.HasIndex("ApplicationId")
|
||||
.HasDatabaseName("ix_tokens_application_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_tokens_user_id");
|
||||
|
||||
b.ToTable("tokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("avatar");
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("custom_preferences");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<Instant>("LastSidReroll")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_sid_reroll");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<bool>("ListHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("list_hidden");
|
||||
|
||||
b.Property<string>("MemberTitle")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<UserSettings>("Settings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("settings");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_user_sid()");
|
||||
|
||||
b.Property<string>("Timezone")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("timezone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_user_flags");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_user_flags_pride_flag_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_flags_user_id");
|
||||
|
||||
b.ToTable("user_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
|
||||
.WithOne("AuditLogEntry")
|
||||
.HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
.WithMany()
|
||||
.HasForeignKey("FediverseApplicationId")
|
||||
.HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("AuthMethods")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_auth_methods_users_user_id");
|
||||
|
||||
b.Navigation("FediverseApplication");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("DataExports")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_data_exports_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("MemberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_members_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifications_users_target_id");
|
||||
|
||||
b.Navigation("Target");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_pride_flags_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReporterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_reporter_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetMemberId")
|
||||
.HasConstraintName("fk_reports_members_target_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_target_user_id");
|
||||
|
||||
b.Navigation("Reporter");
|
||||
|
||||
b.Navigation("TargetMember");
|
||||
|
||||
b.Navigation("TargetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApplicationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_applications_application_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_tokens_users_user_id");
|
||||
|
||||
b.Navigation("Application");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_users_user_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Navigation("AuditLogEntry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("DataExports");
|
||||
|
||||
b.Navigation("Flags");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Foxnouns.Backend.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotices : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "notices",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<long>(type: "bigint", nullable: false),
|
||||
message = table.Column<string>(type: "text", nullable: false),
|
||||
start_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
end_time = table.Column<Instant>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
author_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_notices", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_notices_users_author_id",
|
||||
column: x => x.author_id,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_notices_author_id",
|
||||
table: "notices",
|
||||
column: "author_id"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "notices");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
// <auto-generated />
|
||||
using System.Collections.Generic;
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
@ -17,9 +19,10 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b =>
|
||||
|
@ -43,12 +46,12 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string[]>("RedirectUris")
|
||||
b.PrimitiveCollection<string[]>("RedirectUris")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("redirect_uris");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
@ -59,6 +62,63 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.ToTable("applications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ClearedFields")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("cleared_fields");
|
||||
|
||||
b.Property<long>("ModeratorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("moderator_id");
|
||||
|
||||
b.Property<string>("ModeratorUsername")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("moderator_username");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long?>("ReportId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("report_id");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetMemberName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_member_name");
|
||||
|
||||
b.Property<long?>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.Property<string>("TargetUsername")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_username");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_audit_log");
|
||||
|
||||
b.HasIndex("ReportId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_audit_log_report_id");
|
||||
|
||||
b.ToTable("audit_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
@ -95,9 +155,47 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_auth_methods_user_id");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id")
|
||||
.HasFilter("fediverse_application_id IS NULL");
|
||||
|
||||
b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id")
|
||||
.HasFilter("fediverse_application_id IS NOT NULL");
|
||||
|
||||
b.ToTable("auth_methods", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("filename");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_data_exports");
|
||||
|
||||
b.HasIndex("Filename")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_data_exports_filename");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_data_exports_user_id");
|
||||
|
||||
b.ToTable("data_exports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
@ -119,6 +217,10 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("domain");
|
||||
|
||||
b.Property<bool>("ForceRefresh")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("force_refresh");
|
||||
|
||||
b.Property<int>("InstanceType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("instance_type");
|
||||
|
@ -147,7 +249,19 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
@ -157,6 +271,23 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_member_sid()");
|
||||
|
||||
b.Property<bool>("Unlisted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("unlisted");
|
||||
|
@ -168,6 +299,14 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_members");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_sid");
|
||||
|
||||
b.HasIndex("UserId", "Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_members_user_id_name");
|
||||
|
@ -175,6 +314,203 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.ToTable("members", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("MemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("member_id");
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_member_flags");
|
||||
|
||||
b.HasIndex("MemberId")
|
||||
.HasDatabaseName("ix_member_flags_member_id");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_member_flags_pride_flag_id");
|
||||
|
||||
b.ToTable("member_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("author_id");
|
||||
|
||||
b.Property<Instant>("EndTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("end_time");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<Instant>("StartTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("start_time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notices");
|
||||
|
||||
b.HasIndex("AuthorId")
|
||||
.HasDatabaseName("ix_notices_author_id");
|
||||
|
||||
b.ToTable("notices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant?>("AcknowledgedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("acknowledged_at");
|
||||
|
||||
b.Property<string>("LocalizationKey")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("localization_key");
|
||||
|
||||
b.Property<Dictionary<string, string>>("LocalizationParams")
|
||||
.IsRequired()
|
||||
.HasColumnType("hstore")
|
||||
.HasColumnName("localization_params");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<long>("TargetId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifications");
|
||||
|
||||
b.HasIndex("TargetId")
|
||||
.HasDatabaseName("ix_notifications_target_id");
|
||||
|
||||
b.ToTable("notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pride_flags");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_pride_flags_legacy_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_pride_flags_user_id");
|
||||
|
||||
b.ToTable("pride_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("context");
|
||||
|
||||
b.Property<int>("Reason")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<long>("ReporterId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("reporter_id");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<long?>("TargetMemberId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_member_id");
|
||||
|
||||
b.Property<string>("TargetSnapshot")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("target_snapshot");
|
||||
|
||||
b.Property<int>("TargetType")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("target_type");
|
||||
|
||||
b.Property<long>("TargetUserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("target_user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_reports");
|
||||
|
||||
b.HasIndex("ReporterId")
|
||||
.HasDatabaseName("ix_reports_reporter_id");
|
||||
|
||||
b.HasIndex("TargetMemberId")
|
||||
.HasDatabaseName("ix_reports_target_member_id");
|
||||
|
||||
b.HasIndex("TargetUserId")
|
||||
.HasDatabaseName("ix_reports_target_user_id");
|
||||
|
||||
b.ToTable("reports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
@ -198,7 +534,7 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("boolean")
|
||||
.HasColumnName("manually_expired");
|
||||
|
||||
b.Property<string[]>("Scopes")
|
||||
b.PrimitiveCollection<string[]>("Scopes")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("scopes");
|
||||
|
@ -233,11 +569,48 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("bio");
|
||||
|
||||
b.Property<Dictionary<Snowflake, User.CustomPreference>>("CustomPreferences")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("custom_preferences");
|
||||
|
||||
b.Property<bool>("Deleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deleted");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<long?>("DeletedBy")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("deleted_by");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("display_name");
|
||||
|
||||
b.Property<string[]>("Links")
|
||||
b.Property<List<Field>>("Fields")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("fields");
|
||||
|
||||
b.Property<Instant>("LastActive")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_active");
|
||||
|
||||
b.Property<Instant>("LastSidReroll")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_sid_reroll");
|
||||
|
||||
b.Property<string>("LegacyId")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("legacy_id")
|
||||
.HasDefaultValueSql("gen_random_uuid()");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Links")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("links");
|
||||
|
@ -250,14 +623,40 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.HasColumnType("text")
|
||||
.HasColumnName("member_title");
|
||||
|
||||
b.Property<List<FieldEntry>>("Names")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("names");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<List<Pronoun>>("Pronouns")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("pronouns");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<UserSettings>("Settings")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("settings");
|
||||
|
||||
b.Property<string>("Sid")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("sid")
|
||||
.HasDefaultValueSql("find_free_user_sid()");
|
||||
|
||||
b.Property<string>("Timezone")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("timezone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
|
@ -266,6 +665,14 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.HasKey("Id")
|
||||
.HasName("pk_users");
|
||||
|
||||
b.HasIndex("LegacyId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_legacy_id");
|
||||
|
||||
b.HasIndex("Sid")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_sid");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_username");
|
||||
|
@ -273,6 +680,46 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("PrideFlagId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("pride_flag_id");
|
||||
|
||||
b.Property<long>("UserId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_user_flags");
|
||||
|
||||
b.HasIndex("PrideFlagId")
|
||||
.HasDatabaseName("ix_user_flags_pride_flag_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_flags_user_id");
|
||||
|
||||
b.ToTable("user_flags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report")
|
||||
.WithOne("AuditLogEntry")
|
||||
.HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_audit_log_reports_report_id");
|
||||
|
||||
b.Navigation("Report");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication")
|
||||
|
@ -292,6 +739,18 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
.WithMany("DataExports")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_data_exports_users_user_id");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "User")
|
||||
|
@ -301,75 +760,90 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
.IsRequired()
|
||||
.HasConstraintName("fk_members_users_user_id");
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Field>", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.FieldEntry>", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("System.Collections.Generic.List<Foxnouns.Backend.Database.Models.Pronoun>", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("MemberId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("MemberId");
|
||||
|
||||
b1.ToTable("members");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("MemberId")
|
||||
.HasConstraintName("fk_members_members_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("MemberId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_members_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_member_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Author")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notices_users_author_id");
|
||||
|
||||
b.Navigation("Author");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Target")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_notifications_users_target_id");
|
||||
|
||||
b.Navigation("Target");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("Flags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_pride_flags_users_user_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ReporterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_reporter_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetMemberId")
|
||||
.HasConstraintName("fk_reports_members_target_member_id");
|
||||
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("TargetUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_reports_users_target_user_id");
|
||||
|
||||
b.Navigation("Reporter");
|
||||
|
||||
b.Navigation("TargetMember");
|
||||
|
||||
b.Navigation("TargetUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b =>
|
||||
{
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application")
|
||||
|
@ -391,83 +865,46 @@ namespace Foxnouns.Backend.Database.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b =>
|
||||
{
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag")
|
||||
.WithMany()
|
||||
.HasForeignKey("PrideFlagId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_pride_flags_pride_flag_id");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
b.HasOne("Foxnouns.Backend.Database.Models.User", null)
|
||||
.WithMany("ProfileFlags")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_user_flags_users_user_id");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
b.Navigation("PrideFlag");
|
||||
});
|
||||
|
||||
b1.ToTable("users");
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b =>
|
||||
{
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
|
||||
b1.ToJson("fields");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("names");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 =>
|
||||
{
|
||||
b1.Property<long>("UserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<int>("Capacity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.HasKey("UserId")
|
||||
.HasName("pk_users");
|
||||
|
||||
b1.ToTable("users");
|
||||
|
||||
b1.ToJson("pronouns");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("UserId")
|
||||
.HasConstraintName("fk_users_users_user_id");
|
||||
});
|
||||
|
||||
b.Navigation("Fields")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Names")
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Pronouns")
|
||||
.IsRequired();
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b =>
|
||||
{
|
||||
b.Navigation("AuditLogEntry");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b =>
|
||||
{
|
||||
b.Navigation("AuthMethods");
|
||||
|
||||
b.Navigation("DataExports");
|
||||
|
||||
b.Navigation("Flags");
|
||||
|
||||
b.Navigation("Members");
|
||||
|
||||
b.Navigation("ProfileFlags");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.Security.Cryptography;
|
||||
using Foxnouns.Backend.Utils;
|
||||
|
||||
|
@ -9,22 +23,32 @@ public class Application : BaseModel
|
|||
public required string ClientSecret { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required string[] Scopes { get; init; }
|
||||
public required string[] RedirectUris { get; set; }
|
||||
public required string[] RedirectUris { get; init; }
|
||||
|
||||
public static Application Create(ISnowflakeGenerator snowflakeGenerator, string name, string[] scopes,
|
||||
string[] redirectUrls)
|
||||
public static Application Create(
|
||||
ISnowflakeGenerator snowflakeGenerator,
|
||||
string name,
|
||||
string[] scopes,
|
||||
string[] redirectUrls
|
||||
)
|
||||
{
|
||||
var clientId = RandomNumberGenerator.GetHexString(32, true);
|
||||
var clientSecret = OauthUtils.RandomToken();
|
||||
string clientId = RandomNumberGenerator.GetHexString(32, true);
|
||||
string clientSecret = AuthUtils.RandomToken();
|
||||
|
||||
if (scopes.Except(OauthUtils.ApplicationScopes).Any())
|
||||
if (scopes.Except(AuthUtils.ApplicationScopes).Any())
|
||||
{
|
||||
throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes));
|
||||
throw new ArgumentException(
|
||||
"Invalid scopes passed to Application.Create",
|
||||
nameof(scopes)
|
||||
);
|
||||
}
|
||||
|
||||
if (redirectUrls.Any(s => !OauthUtils.ValidateRedirectUri(s)))
|
||||
if (redirectUrls.Any(s => !AuthUtils.ValidateRedirectUri(s)))
|
||||
{
|
||||
throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls));
|
||||
throw new ArgumentException(
|
||||
"Invalid redirect URLs passed to Application.Create",
|
||||
nameof(redirectUrls)
|
||||
);
|
||||
}
|
||||
|
||||
return new Application
|
||||
|
@ -34,7 +58,7 @@ public class Application : BaseModel
|
|||
ClientSecret = clientSecret,
|
||||
Name = name,
|
||||
Scopes = scopes,
|
||||
RedirectUris = redirectUrls
|
||||
RedirectUris = redirectUrls,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
45
Foxnouns.Backend/Database/Models/AuditLogEntry.cs
Normal file
45
Foxnouns.Backend/Database/Models/AuditLogEntry.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class AuditLogEntry : BaseModel
|
||||
{
|
||||
public Snowflake ModeratorId { get; init; }
|
||||
public string ModeratorUsername { get; init; } = string.Empty;
|
||||
public Snowflake? TargetUserId { get; init; }
|
||||
public string? TargetUsername { get; init; }
|
||||
public Snowflake? TargetMemberId { get; init; }
|
||||
public string? TargetMemberName { get; init; }
|
||||
public Snowflake? ReportId { get; init; }
|
||||
public Report? Report { get; init; }
|
||||
|
||||
public AuditLogEntryType Type { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public string[]? ClearedFields { get; init; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum AuditLogEntryType
|
||||
{
|
||||
IgnoreReport,
|
||||
WarnUser,
|
||||
WarnUserAndClearProfile,
|
||||
SuspendUser,
|
||||
QuerySensitiveUserData,
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class AuthMethod : BaseModel
|
||||
|
@ -20,4 +34,4 @@ public enum AuthType
|
|||
Tumblr,
|
||||
Fediverse,
|
||||
Email,
|
||||
}
|
||||
}
|
||||
|
|
26
Foxnouns.Backend/Database/Models/DataExport.cs
Normal file
26
Foxnouns.Backend/Database/Models/DataExport.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class DataExport : BaseModel
|
||||
{
|
||||
public Snowflake UserId { get; init; }
|
||||
public User User { get; init; } = null!;
|
||||
public required string Filename { get; init; }
|
||||
|
||||
public static readonly Duration Expiration = Duration.FromDays(15);
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class FediverseApplication : BaseModel
|
||||
|
@ -6,10 +20,11 @@ public class FediverseApplication : BaseModel
|
|||
public required string ClientId { get; set; }
|
||||
public required string ClientSecret { get; set; }
|
||||
public required FediverseInstanceType InstanceType { get; set; }
|
||||
public bool ForceRefresh { get; set; }
|
||||
}
|
||||
|
||||
public enum FediverseInstanceType
|
||||
{
|
||||
MastodonApi,
|
||||
MisskeyApi
|
||||
}
|
||||
MisskeyApi,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Field
|
||||
|
@ -15,4 +29,4 @@ public class FieldEntry
|
|||
public class Pronoun : FieldEntry
|
||||
{
|
||||
public string? DisplayText { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,24 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Member : BaseModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
|
@ -13,6 +29,8 @@ public class Member : BaseModel
|
|||
public List<Pronoun> Pronouns { get; set; } = [];
|
||||
public List<Field> Fields { get; set; } = [];
|
||||
|
||||
public List<MemberFlag> ProfileFlags { get; set; } = [];
|
||||
|
||||
public Snowflake UserId { get; init; }
|
||||
public User User { get; init; } = null!;
|
||||
}
|
||||
}
|
||||
|
|
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
13
Foxnouns.Backend/Database/Models/Notice.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Notice : BaseModel
|
||||
{
|
||||
public required string Message { get; set; }
|
||||
public required Instant StartTime { get; set; }
|
||||
public required Instant EndTime { get; set; }
|
||||
|
||||
public Snowflake AuthorId { get; init; }
|
||||
public User Author { get; init; } = null!;
|
||||
}
|
41
Foxnouns.Backend/Database/Models/Notification.cs
Normal file
41
Foxnouns.Backend/Database/Models/Notification.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Notification : BaseModel
|
||||
{
|
||||
public Snowflake TargetId { get; init; }
|
||||
public User Target { get; init; } = null!;
|
||||
|
||||
public NotificationType Type { get; init; }
|
||||
|
||||
public string? Message { get; init; }
|
||||
public string? LocalizationKey { get; init; }
|
||||
public Dictionary<string, string> LocalizationParams { get; init; } = [];
|
||||
|
||||
public Instant? AcknowledgedAt { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum NotificationType
|
||||
{
|
||||
Notice,
|
||||
Warning,
|
||||
Suspension,
|
||||
}
|
42
Foxnouns.Backend/Database/Models/PrideFlag.cs
Normal file
42
Foxnouns.Backend/Database/Models/PrideFlag.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class PrideFlag : BaseModel
|
||||
{
|
||||
public required Snowflake UserId { get; init; }
|
||||
public required string LegacyId { get; init; }
|
||||
|
||||
// A null hash means the flag hasn't been processed yet.
|
||||
public string? Hash { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public class UserFlag
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required Snowflake UserId { get; init; }
|
||||
public required Snowflake PrideFlagId { get; init; }
|
||||
public PrideFlag PrideFlag { get; init; } = null!;
|
||||
}
|
||||
|
||||
public class MemberFlag
|
||||
{
|
||||
public long Id { get; init; }
|
||||
public required Snowflake MemberId { get; init; }
|
||||
public required Snowflake PrideFlagId { get; init; }
|
||||
public PrideFlag PrideFlag { get; init; } = null!;
|
||||
}
|
76
Foxnouns.Backend/Database/Models/Report.cs
Normal file
76
Foxnouns.Backend/Database/Models/Report.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class Report : BaseModel
|
||||
{
|
||||
public Snowflake ReporterId { get; init; }
|
||||
public User Reporter { get; init; } = null!;
|
||||
public Snowflake TargetUserId { get; init; }
|
||||
public User TargetUser { get; init; } = null!;
|
||||
|
||||
public Snowflake? TargetMemberId { get; init; }
|
||||
public Member? TargetMember { get; init; }
|
||||
|
||||
public ReportStatus Status { get; set; }
|
||||
public ReportReason Reason { get; init; }
|
||||
public string? Context { get; init; }
|
||||
|
||||
public ReportTargetType TargetType { get; init; }
|
||||
public string? TargetSnapshot { get; init; }
|
||||
|
||||
public AuditLogEntry? AuditLogEntry { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum ReportTargetType
|
||||
{
|
||||
User,
|
||||
Member,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum ReportStatus
|
||||
{
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public enum ReportReason
|
||||
{
|
||||
Totalitarianism,
|
||||
HateSpeech,
|
||||
Racism,
|
||||
Homophobia,
|
||||
Transphobia,
|
||||
Queerphobia,
|
||||
Exclusionism,
|
||||
Sexism,
|
||||
Ableism,
|
||||
ChildPornography,
|
||||
PedophiliaAdvocacy,
|
||||
Harassment,
|
||||
Impersonation,
|
||||
Doxxing,
|
||||
EncouragingSelfHarm,
|
||||
Spam,
|
||||
Trolling,
|
||||
Advertisement,
|
||||
CopyrightViolation,
|
||||
}
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
@ -14,4 +28,4 @@ public class Token : BaseModel
|
|||
|
||||
public Snowflake ApplicationId { get; set; }
|
||||
public Application Application { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,81 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database.Models;
|
||||
|
||||
public class User : BaseModel
|
||||
{
|
||||
public required string Username { get; set; }
|
||||
public string Sid { get; set; } = string.Empty;
|
||||
public required string LegacyId { get; init; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? MemberTitle { get; set; }
|
||||
public string? Avatar { get; set; }
|
||||
public string[] Links { get; set; } = [];
|
||||
public bool ListHidden { get; set; }
|
||||
public string? Timezone { get; set; }
|
||||
|
||||
public List<FieldEntry> Names { get; set; } = [];
|
||||
public List<Pronoun> Pronouns { get; set; } = [];
|
||||
public List<Field> Fields { get; set; } = [];
|
||||
public Dictionary<Snowflake, CustomPreference> CustomPreferences { get; set; } = [];
|
||||
|
||||
public List<PrideFlag> Flags { get; set; } = [];
|
||||
public List<UserFlag> ProfileFlags { get; set; } = [];
|
||||
|
||||
public UserRole Role { get; set; } = UserRole.User;
|
||||
public string? Password { get; set; } // Password may be null if the user doesn't authenticate with an email address
|
||||
|
||||
public List<Member> Members { get; } = [];
|
||||
public List<AuthMethod> AuthMethods { get; } = [];
|
||||
public List<DataExport> DataExports { get; } = [];
|
||||
public UserSettings Settings { get; set; } = new();
|
||||
|
||||
public required Instant LastActive { get; set; }
|
||||
public Instant LastSidReroll { get; set; }
|
||||
|
||||
public bool Deleted { get; set; }
|
||||
public Instant? DeletedAt { get; set; }
|
||||
public Snowflake? DeletedBy { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public bool? SelfDelete => Deleted ? DeletedBy != null : null;
|
||||
|
||||
public class CustomPreference
|
||||
{
|
||||
public required string Icon { get; set; }
|
||||
public required string Tooltip { get; set; }
|
||||
public bool Muted { get; set; }
|
||||
public bool Favourite { get; set; }
|
||||
|
||||
// This type is generally serialized directly, so the converter is applied here.
|
||||
[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))]
|
||||
public PreferenceSize Size { get; set; }
|
||||
|
||||
public Guid LegacyId { get; init; } = Guid.NewGuid();
|
||||
}
|
||||
|
||||
public static readonly Duration DeleteAfter = Duration.FromDays(30);
|
||||
public static readonly Duration DeleteSuspendedAfter = Duration.FromDays(180);
|
||||
}
|
||||
|
||||
public enum UserRole
|
||||
|
@ -26,4 +83,17 @@ public enum UserRole
|
|||
User,
|
||||
Moderator,
|
||||
Admin,
|
||||
}
|
||||
}
|
||||
|
||||
public enum PreferenceSize
|
||||
{
|
||||
Large,
|
||||
Normal,
|
||||
Small,
|
||||
}
|
||||
|
||||
public class UserSettings
|
||||
{
|
||||
public bool? DarkMode { get; set; }
|
||||
public Snowflake? LastReadNotice { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,12 +1,32 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
||||
[JsonConverter(typeof(JsonConverter))]
|
||||
public readonly struct Snowflake(ulong value)
|
||||
[System.Text.Json.Serialization.JsonConverter(typeof(SystemJsonConverter))]
|
||||
[TypeConverter(typeof(TypeConverter))]
|
||||
public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
|
||||
{
|
||||
public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC
|
||||
public readonly ulong Value = value;
|
||||
|
@ -37,47 +57,102 @@ public readonly struct Snowflake(ulong value)
|
|||
public short Increment => (short)(Value & 0xFFF);
|
||||
|
||||
public static bool operator <(Snowflake arg1, Snowflake arg2) => arg1.Value < arg2.Value;
|
||||
|
||||
public static bool operator >(Snowflake arg1, Snowflake arg2) => arg1.Value > arg2.Value;
|
||||
|
||||
public static bool operator ==(Snowflake arg1, Snowflake arg2) => arg1.Value == arg2.Value;
|
||||
|
||||
public static bool operator !=(Snowflake arg1, Snowflake arg2) => arg1.Value != arg2.Value;
|
||||
|
||||
public static implicit operator ulong(Snowflake s) => s.Value;
|
||||
|
||||
public static implicit operator long(Snowflake s) => (long)s.Value;
|
||||
|
||||
public static implicit operator Snowflake(ulong n) => new(n);
|
||||
|
||||
public static implicit operator Snowflake(long n) => new((ulong)n);
|
||||
|
||||
public static bool TryParse(string input, [NotNullWhen(true)] out Snowflake? snowflake)
|
||||
{
|
||||
snowflake = null;
|
||||
if (!ulong.TryParse(input, out var res)) return false;
|
||||
if (!ulong.TryParse(input, out ulong res))
|
||||
return false;
|
||||
snowflake = new Snowflake(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Snowflake FromInstant(Instant instant) =>
|
||||
new((ulong)(instant.ToUnixTimeMilliseconds() - Epoch) << 22);
|
||||
|
||||
public override bool Equals(object? obj) => obj is Snowflake other && Value == other.Value;
|
||||
|
||||
public bool Equals(Snowflake other) => Value == other.Value;
|
||||
|
||||
public override int GetHashCode() => Value.GetHashCode();
|
||||
|
||||
public override string ToString() => Value.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// An Entity Framework ValueConverter for Snowflakes to longs.
|
||||
/// </summary>
|
||||
// ReSharper disable once ClassNeverInstantiated.Global
|
||||
public class ValueConverter() : ValueConverter<Snowflake, long>(
|
||||
convertToProviderExpression: x => x,
|
||||
convertFromProviderExpression: x => x
|
||||
);
|
||||
public class ValueConverter() : ValueConverter<Snowflake, long>(x => x, x => x);
|
||||
|
||||
private class JsonConverter : JsonConverter<Snowflake>
|
||||
private class SystemJsonConverter : System.Text.Json.Serialization.JsonConverter<Snowflake>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Snowflake value, JsonSerializer serializer)
|
||||
public override Snowflake Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options
|
||||
) => ulong.Parse(reader.GetString()!);
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
Snowflake value,
|
||||
JsonSerializerOptions options
|
||||
) => writer.WriteStringValue(value.Value.ToString());
|
||||
}
|
||||
|
||||
private class JsonConverter : JsonConverter<Snowflake?>
|
||||
{
|
||||
public override void WriteJson(
|
||||
JsonWriter writer,
|
||||
Snowflake? value,
|
||||
JsonSerializer serializer
|
||||
)
|
||||
{
|
||||
writer.WriteValue(value.Value.ToString());
|
||||
if (value != null)
|
||||
writer.WriteValue(value.Value.ToString());
|
||||
else
|
||||
writer.WriteNull();
|
||||
}
|
||||
|
||||
public override Snowflake ReadJson(JsonReader reader, Type objectType, Snowflake existingValue,
|
||||
public override Snowflake? ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Snowflake? existingValue,
|
||||
bool hasExistingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
return ulong.Parse((string)reader.Value!);
|
||||
}
|
||||
JsonSerializer serializer
|
||||
) =>
|
||||
reader.TokenType is not (JsonToken.None or JsonToken.Null)
|
||||
? ulong.Parse((string)reader.Value!)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
private class TypeConverter : System.ComponentModel.TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
|
||||
sourceType == typeof(string);
|
||||
|
||||
public override bool CanConvertTo(
|
||||
ITypeDescriptorContext? context,
|
||||
[NotNullWhen(true)] Type? destinationType
|
||||
) => destinationType == typeof(Snowflake);
|
||||
|
||||
public override object? ConvertFrom(
|
||||
ITypeDescriptorContext? context,
|
||||
CultureInfo? culture,
|
||||
object value
|
||||
) => TryParse((string)value, out Snowflake? snowflake) ? snowflake : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Database;
|
||||
|
@ -28,18 +42,21 @@ public class SnowflakeGenerator : ISnowflakeGenerator
|
|||
public Snowflake GenerateSnowflake(Instant? time = null)
|
||||
{
|
||||
time ??= SystemClock.Instance.GetCurrentInstant();
|
||||
var increment = Interlocked.Increment(ref _increment);
|
||||
var threadId = Environment.CurrentManagedThreadId % 32;
|
||||
var timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch;
|
||||
long increment = Interlocked.Increment(ref _increment);
|
||||
int threadId = Environment.CurrentManagedThreadId % 32;
|
||||
long timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch;
|
||||
|
||||
return (timestamp << 22) | (uint)(_processId << 17) | (uint)(threadId << 12) | (increment % 4096);
|
||||
return (timestamp << 22)
|
||||
| (uint)(_processId << 17)
|
||||
| (uint)(threadId << 12)
|
||||
| (increment % 4096);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SnowflakeGeneratorServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddSnowflakeGenerator(this IServiceCollection services, int? processId = null)
|
||||
{
|
||||
return services.AddSingleton<ISnowflakeGenerator>(new SnowflakeGenerator(processId));
|
||||
}
|
||||
}
|
||||
public static IServiceCollection AddSnowflakeGenerator(
|
||||
this IServiceCollection services,
|
||||
int? processId = null
|
||||
) => services.AddSingleton<ISnowflakeGenerator>(new SnowflakeGenerator(processId));
|
||||
}
|
||||
|
|
30
Foxnouns.Backend/Database/prune-designer-cs-files.sh
Normal file
30
Foxnouns.Backend/Database/prune-designer-cs-files.sh
Normal file
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Original script by zotan for Iceshrimp.NET
|
||||
# Source: https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/commit/7c93dcf79dda54fc1a4ea9772e3f80874e6bcefb/Iceshrimp.Backend/Core/Database/prune-designer-cs-files.sh
|
||||
|
||||
if [[ $(uname) == "Darwin" ]]; then
|
||||
SED="gsed"
|
||||
else
|
||||
SED="sed"
|
||||
fi
|
||||
|
||||
import="using Microsoft.EntityFrameworkCore.Infrastructure;"
|
||||
dbc=" [DbContext(typeof(DatabaseContext))]"
|
||||
|
||||
for file in $(find "$(dirname $0)/Migrations" -name '*.Designer.cs'); do
|
||||
echo "$file"
|
||||
csfile="${file%.Designer.cs}.cs"
|
||||
if [[ ! -f $csfile ]]; then
|
||||
echo "$csfile doesn't exist, exiting"
|
||||
exit 1
|
||||
fi
|
||||
lineno=$($SED -n '/^{/=' "$csfile")
|
||||
((lineno+=2))
|
||||
migr=$(grep "\[Migration" "$file")
|
||||
$SED -i "${lineno}i \\$migr" "$csfile"
|
||||
$SED -i "${lineno}i \\$dbc" "$csfile"
|
||||
$SED -i "2i $import" "$csfile"
|
||||
rm "$file"
|
||||
done
|
66
Foxnouns.Backend/Dto/Auth.cs
Normal file
66
Foxnouns.Backend/Dto/Auth.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Database.Models;
|
||||
using Foxnouns.Backend.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
public record CallbackResponse(
|
||||
bool HasAccount,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] UserResponse? User,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt
|
||||
);
|
||||
|
||||
public record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr);
|
||||
|
||||
public record AuthResponse(UserResponse User, string Token, Instant ExpiresAt);
|
||||
|
||||
public record SingleUrlResponse(string Url);
|
||||
|
||||
public record AddOauthAccountResponse(
|
||||
Snowflake Id,
|
||||
[property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type,
|
||||
string RemoteId,
|
||||
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername
|
||||
);
|
||||
|
||||
public record OauthRegisterRequest(string Ticket, string Username);
|
||||
|
||||
public record CallbackRequest(string Code, string State);
|
||||
|
||||
public record EmailLoginRequest(string Email, string Password);
|
||||
|
||||
public record EmailRegisterRequest(string Email);
|
||||
|
||||
public record EmailCompleteRegistrationRequest(string Ticket, string Username, string Password);
|
||||
|
||||
public record EmailCallbackRequest(string State);
|
||||
|
||||
public record EmailChangePasswordRequest(string Current, string New);
|
||||
|
||||
public record EmailForgotPasswordRequest(string Email);
|
||||
|
||||
public record EmailResetPasswordRequest(string State, string Password);
|
||||
|
||||
public record FediverseCallbackRequest(string Instance, string Code, string? State = null);
|
21
Foxnouns.Backend/Dto/DataExport.cs
Normal file
21
Foxnouns.Backend/Dto/DataExport.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
using NodaTime;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
public record DataExportResponse(string? Url, Instant? ExpiresAt);
|
31
Foxnouns.Backend/Dto/Flag.cs
Normal file
31
Foxnouns.Backend/Dto/Flag.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
using Foxnouns.Backend.Database;
|
||||
using Foxnouns.Backend.Utils;
|
||||
|
||||
namespace Foxnouns.Backend.Dto;
|
||||
|
||||
public record PrideFlagResponse(Snowflake Id, string? ImageUrl, string Name, string? Description);
|
||||
|
||||
public record CreateFlagRequest(string Name, string Image, string? Description);
|
||||
|
||||
public class UpdateFlagRequest : PatchRequest
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue