Add CORS to api

This commit is contained in:
Kieran 2022-02-26 14:22:22 +00:00
parent 36d5db3f29
commit 67b5ef2b10
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 51 additions and 32 deletions

View File

@ -75,7 +75,8 @@ public class AuthController : Controller
var claims = new List<Claim>() var claims = new List<Claim>()
{ {
new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Expiration, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString()) new(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString()),
new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
}; };
claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a))); claims.AddRange(user.Roles.Select(a => new Claim(ClaimTypes.Role, a)));

View File

@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Prometheus;
using VoidCat.Model; using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Abstractions; using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers namespace VoidCat.Controllers
@ -19,7 +17,7 @@ namespace VoidCat.Controllers
} }
[HttpGet] [HttpGet]
[ResponseCache(Location = ResponseCacheLocation.Client, Duration = 60)] [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
public async Task<GlobalStats> GetGlobalStats() public async Task<GlobalStats> GetGlobalStats()
{ {
var bw = await _statsReporter.GetBandwidth(); var bw = await _statsReporter.GetBandwidth();

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -8,6 +9,7 @@ using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers namespace VoidCat.Controllers
{ {
[EnableCors(CorsPolicy.Upload)]
[Route("upload")] [Route("upload")]
public class UploadController : Controller public class UploadController : Controller
{ {
@ -25,14 +27,6 @@ namespace VoidCat.Controllers
_paywallFactory = paywallFactory; _paywallFactory = paywallFactory;
} }
[HttpOptions]
public IActionResult UploadFileOptions()
{
// just return 200 status for pre-flight calls
// manging CORS headers is not managed inside void.cat
return Ok();
}
[HttpPost] [HttpPost]
[DisableRequestSizeLimit] [DisableRequestSizeLimit]
[DisableFormValueModelBinding] [DisableFormValueModelBinding]

View File

@ -10,3 +10,9 @@ public static class Policies
{ {
public const string RequireAdmin = "RequireAdmin"; public const string RequireAdmin = "RequireAdmin";
} }
public static class CorsPolicy
{
public const string Default = "default";
public const string Upload = "upload";
}

View File

@ -15,6 +15,8 @@ namespace VoidCat.Model
public StrikeApiSettings? Strike { get; init; } public StrikeApiSettings? Strike { get; init; }
public SmtpSettings? Smtp { get; init; } public SmtpSettings? Smtp { get; init; }
public List<Uri> CorsOrigins { get; init; } = new();
} }
public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword); public sealed record TorSettings(Uri TorControl, string PrivateKey, string ControlPassword);

View File

@ -33,6 +33,24 @@ if (useRedis)
services.AddSingleton(cx.GetDatabase()); services.AddSingleton(cx.GetDatabase());
} }
services.AddCors(opt =>
{
opt.AddPolicy(CorsPolicy.Default, p =>
{
p.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins(voidSettings.CorsOrigins.Select(a => a.OriginalString).ToArray());
});
opt.AddPolicy(CorsPolicy.Upload, p =>
{
p.AllowCredentials()
.AllowAnyMethod()
.WithHeaders("V-Content-Type", "V-Filename", "V-Digest", "Content-Type", "Authorization")
.WithOrigins(voidSettings.CorsOrigins.Select(a => a.OriginalString).ToArray());
});
});
services.AddRouting(); services.AddRouting();
services.AddControllers().AddNewtonsoftJson((opt) => services.AddControllers().AddNewtonsoftJson((opt) =>
{ {
@ -112,6 +130,7 @@ app.UseStaticFiles();
#endif #endif
app.UseRouting(); app.UseRouting();
app.UseCors(CorsPolicy.Default);
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@ -1,6 +1,6 @@
import moment from "moment"; import moment from "moment";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch} from "react-redux";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {FormatBytes} from "../Util"; import {FormatBytes} from "../Util";
import {useApi} from "../Api"; import {useApi} from "../Api";
@ -8,9 +8,8 @@ import {logout} from "../LoginState";
import {PagedSortBy, PageSortOrder} from "../Const"; import {PagedSortBy, PageSortOrder} from "../Const";
import {PageSelector} from "../PageSelector"; import {PageSelector} from "../PageSelector";
export function FileList(props) { export function FileList() {
const {AdminApi} = useApi(); const {AdminApi} = useApi();
const auth = useSelector((state) => state.login.jwt);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [files, setFiles] = useState(); const [files, setFiles] = useState();
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@ -24,7 +23,7 @@ export function FileList(props) {
sortBy: PagedSortBy.Date, sortBy: PagedSortBy.Date,
sortOrder: PageSortOrder.Dsc sortOrder: PageSortOrder.Dsc
}; };
let req = await AdminApi.fileList(auth, pageReq); let req = await AdminApi.fileList(pageReq);
if (req.ok) { if (req.ok) {
setFiles(await req.json()); setFiles(await req.json());
} else if (req.status === 401) { } else if (req.status === 401) {
@ -37,7 +36,7 @@ export function FileList(props) {
async function deleteFile(e, id) { async function deleteFile(e, id) {
e.target.disabled = true; e.target.disabled = true;
if (window.confirm(`Are you sure you want to delete: ${id}?`)) { if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
let req = await AdminApi.deleteFile(auth, id); let req = await AdminApi.deleteFile(id);
if (req.ok) { if (req.ok) {
setFiles({ setFiles({
...files, ...files,

View File

@ -1,4 +1,4 @@
import {useDispatch, useSelector} from "react-redux"; import {useDispatch} from "react-redux";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {PagedSortBy, PageSortOrder} from "../Const"; import {PagedSortBy, PageSortOrder} from "../Const";
import {useApi} from "../Api"; import {useApi} from "../Api";
@ -8,7 +8,6 @@ import moment from "moment";
export function UserList() { export function UserList() {
const {AdminApi} = useApi(); const {AdminApi} = useApi();
const auth = useSelector((state) => state.login.jwt);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [users, setUsers] = useState(); const [users, setUsers] = useState();
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@ -22,12 +21,12 @@ export function UserList() {
sortBy: PagedSortBy.Id, sortBy: PagedSortBy.Id,
sortOrder: PageSortOrder.Asc sortOrder: PageSortOrder.Asc
}; };
let req = await AdminApi.userList(auth, pageReq); let req = await AdminApi.userList(pageReq);
if (req.ok) { if (req.ok) {
setUsers(await req.json()); setUsers(await req.json());
} else if (req.status === 401) { } else if (req.status === 401) {
dispatch(logout()); dispatch(logout());
} else if(req.status === 403) { } else if (req.status === 403) {
setAccessDenied(true); setAccessDenied(true);
} }
} }

View File

@ -4,12 +4,12 @@ import {ApiHost} from "./Const";
export function useApi() { export function useApi() {
const auth = useSelector(state => state.login.jwt); const auth = useSelector(state => state.login.jwt);
async function getJson(method, url, body) { async function getJson(method, url, body, token) {
let headers = { let headers = {
"Accept": "application/json" "Accept": "application/json"
}; };
if (auth) { if (token) {
headers["Authorization"] = `Bearer ${auth}`; headers["Authorization"] = `Bearer ${token}`;
} }
if (body) { if (body) {
headers["Content-Type"] = "application/json"; headers["Content-Type"] = "application/json";
@ -18,20 +18,21 @@ export function useApi() {
return await fetch(`${ApiHost}${url}`, { return await fetch(`${ApiHost}${url}`, {
method, method,
headers, headers,
mode: "cors",
body: body ? JSON.stringify(body) : undefined body: body ? JSON.stringify(body) : undefined
}); });
} }
return { return {
AdminApi: { AdminApi: {
fileList: (pageReq) => getJson("POST", "/admin/file", pageReq), fileList: (pageReq) => getJson("POST", "/admin/file", pageReq, auth),
deleteFile: (id) => getJson("DELETE", `/admin/file/${id}`), deleteFile: (id) => getJson("DELETE", `/admin/file/${id}`, undefined, auth),
userList: (pageReq) => getJson("POST", `/admin/user`, pageReq) userList: (pageReq) => getJson("POST", `/admin/user`, pageReq, auth)
}, },
Api: { Api: {
stats: () => getJson("GET", "/stats"), stats: () => getJson("GET", "/stats"),
fileInfo: (id) => getJson("GET", `/upload/${id}`), fileInfo: (id) => getJson("GET", `/upload/${id}`),
setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg), setPaywallConfig: (id, cfg) => getJson("POST", `/upload/${id}/paywall`, cfg, auth),
createOrder: (id) => getJson("GET", `/upload/${id}/paywall`), createOrder: (id) => getJson("GET", `/upload/${id}/paywall`),
getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`), getOrder: (file, order) => getJson("GET", `/upload/${file}/paywall/${order}`),
login: (username, password) => getJson("POST", `/auth/login`, {username, password}), login: (username, password) => getJson("POST", `/auth/login`, {username, password}),