el authentication

master
Lea 2023-07-24 23:03:23 +02:00
parent e7da5215c3
commit 2aa2ac18ca
5 changed files with 211 additions and 86 deletions

View File

@ -9,8 +9,8 @@ import {
import { AuthenticationData, AuthGuard, NoAuthGuard } from './auth.guard';
import {
ApiBearerAuth,
ApiForbiddenResponse,
ApiUnauthorizedResponse,
// ApiForbiddenResponse,
// ApiUnauthorizedResponse,
} from '@nestjs/swagger';
export const AuthData = createParamDecorator(
@ -28,12 +28,12 @@ export function Auth() {
return applyDecorators(
UseGuards(AuthGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({
description: 'No valid authentication presented',
}),
ApiForbiddenResponse({
description: 'The user does not have access to the resource',
}),
// ApiUnauthorizedResponse({
// description: 'No valid authentication presented',
// }),
// ApiForbiddenResponse({
// description: 'The user does not have access to the resource',
// }),
);
}
@ -49,8 +49,8 @@ export function ServerAuth(level: 'member' | 'moderator' | 'administrator') {
export function NoAuthentication() {
return applyDecorators(
UseGuards(NoAuthGuard),
ApiForbiddenResponse({
description: 'Access to the resource is forbidden',
}),
// ApiForbiddenResponse({
// description: 'Access to the resource is forbidden',
// }),
);
}

View File

@ -72,12 +72,6 @@
"responses": {
"200": {
"description": ""
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"tags": [
@ -115,12 +109,6 @@
"responses": {
"201": {
"description": ""
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"tags": [
@ -158,12 +146,6 @@
"responses": {
"200": {
"description": ""
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"tags": [
@ -182,12 +164,6 @@
"summary": "List all servers both AutoMod and the user are in",
"parameters": [],
"responses": {
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
},
"default": {
"description": "",
"content": {
@ -224,12 +200,6 @@
}
],
"responses": {
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
},
"default": {
"description": "",
"content": {
@ -266,12 +236,6 @@
}
}
}
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"tags": [
@ -299,9 +263,6 @@
}
},
"responses": {
"403": {
"description": "Access to the resource is forbidden"
},
"default": {
"description": "",
"content": {

View File

@ -8,11 +8,11 @@ export type APIRoutes =
| { method: 'post', path: '-/servers/{server}/infractions', parts: 3, params: paths['/servers/{server}/infractions']['post']['requestBody']['content']['application/json'], response: undefined }
| { method: 'get', path: `/servers/${string}/infractions/${string}`, parts: 4, params: undefined, response: undefined }
| { method: 'get', path: '-/servers/{server}/infractions/{id}', parts: 4, params: undefined, response: undefined }
| { method: 'get', path: `/servers`, parts: 1, params: undefined, response: undefined }
| { method: 'get', path: `/servers/${string}`, parts: 2, params: undefined, response: undefined }
| { method: 'get', path: '-/servers/{server}', parts: 2, params: undefined, response: undefined }
| { method: 'get', path: `/servers`, parts: 1, params: undefined, response: paths['/servers']['get']['responses']['default']['content']['application/json'] }
| { method: 'get', path: `/servers/${string}`, parts: 2, params: undefined, response: paths['/servers/{server}']['get']['responses']['default']['content']['application/json'] }
| { method: 'get', path: '-/servers/{server}', parts: 2, params: undefined, response: paths['/servers/{server}']['get']['responses']['default']['content']['application/json'] }
| { method: 'get', path: `/auth`, parts: 1, params: undefined, response: paths['/auth']['get']['responses']['200']['content']['application/json'] }
| { method: 'post', path: `/auth/login`, parts: 2, params: paths['/auth/login']['post']['requestBody']['content']['application/json'], response: undefined }
| { method: 'post', path: `/auth/login`, parts: 2, params: paths['/auth/login']['post']['requestBody']['content']['application/json'], response: paths['/auth/login']['post']['responses']['default']['content']['application/json'] }
| { method: 'get', path: `/test`, parts: 1, params: undefined, response: undefined }
| { method: 'post', path: `/test`, parts: 1, params: undefined, response: undefined }
| { method: 'patch', path: `/test`, parts: 1, params: undefined, response: undefined }

View File

@ -136,10 +136,6 @@ export interface operations {
};
responses: {
200: never;
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
};
};
/** Create an infraction */
@ -156,10 +152,6 @@ export interface operations {
};
responses: {
201: never;
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
};
};
/** Fetch a single infraction */
@ -172,19 +164,11 @@ export interface operations {
};
responses: {
200: never;
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
};
};
/** List all servers both AutoMod and the user are in */
listServers: {
responses: {
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
default: {
content: {
"application/json": components["schemas"]["GetServersResponse"];
@ -200,10 +184,6 @@ export interface operations {
};
};
responses: {
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
default: {
content: {
"application/json": components["schemas"]["ServerDetails"];
@ -219,10 +199,6 @@ export interface operations {
"application/json": components["schemas"]["AuthInfo"];
};
};
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
};
};
login: {
@ -232,8 +208,6 @@ export interface operations {
};
};
responses: {
/** @description Access to the resource is forbidden */
403: never;
default: {
content: {
"application/json": components["schemas"]["LoginResponse"];

View File

@ -1,24 +1,214 @@
"use client";
import { useBearStore } from "@/components/state";
import { setCredentials } from "@/lib/auth";
import useAuth from "@/lib/useAuth";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { API } from "oapilib";
import { useEffect, useState } from "react";
type AuthWsCallback = (
result: "approved" | "denied" | "errored",
error?: any,
) => any;
class AuthWs {
private ws: WebSocket | null = null;
private done = false;
private token: string;
private cb: AuthWsCallback;
constructor(token: string, cb: AuthWsCallback) {
this.token = token;
this.cb = cb;
this.connect();
}
private connect() {
console.debug("Connecting to WS");
if (this.ws) {
try {
this.ws.close();
} catch (e) {}
}
let uri = `${
new API().config.baseURL
}/auth/ws?auth=${encodeURIComponent(this.token)}`;
if (uri.startsWith("http://")) uri = uri.replace("http://", "ws://");
else if (uri.startsWith("https://"))
uri = uri.replace("https://", "wss://");
else throw new Error("Invalid URL protocol");
this.ws = new WebSocket(uri);
this.ws.onopen = () => {
console.debug("WS connected!");
};
this.ws.onclose = () => {
if (this.done) {
console.debug("WS closed, not reconnecting");
} else {
console.debug("WS closed, reconnecting in 5 seconds...");
setTimeout(() => {
this.connect();
}, 5000);
}
};
this.ws.onmessage = (event) => {
console.debug("Received WS message");
const message = JSON.parse(event.data);
console.debug(message);
if (message.type == "error") {
this.done = true;
this.cb("errored", message);
} else if (message.action) {
this.done = true;
if (message.action == "approved") {
this.cb("approved");
} else {
this.cb("denied");
}
}
};
}
}
export default function Auth() {
const state = useBearStore();
const router = useRouter();
const auth = useAuth({ redirect: false });
const [pageState, setPageState] = useState<
"input" | "loading" | "listening" | "denied" | "error"
>("input");
const [otp, setOtp] = useState("");
const [token, setToken] = useState("");
const [ws, setWs] = useState<AuthWs | null>(null);
const [error, setError] = useState("");
const [uid, setUid] = useState(" ");
useEffect(() => {
if (auth?.authenticated) {
router.push("/dashboard");
}
});
return (
<>
<h1>Let's get you logged in.</h1>
</>
);
switch (pageState) {
case "input":
return (
<>
<h1>Let's get you logged in.</h1>
<label htmlFor="uid">Enter your Revolt ID: </label>
<input
type="text"
id="uid"
value={uid}
onChange={(e) => setUid(e.currentTarget.value)}
/>{" "}
<button
onClick={async () => {
if (!uid) return;
// Let's use a fresh API instance just in case the current one still has authentication set
const api = new API();
setPageState("loading");
try {
const result = await api.post("/auth/login", {
user: uid.trim(),
});
setPageState("listening");
setOtp(result.otp);
setToken(result.token);
setWs(
new AuthWs(
result.token,
async (res, error) => {
if (res == "errored") {
setPageState("error");
setError(JSON.stringify(error));
}
if (res == "approved") {
await setCredentials({
token: result.token,
});
state.api = new API({
authentication: {
headers: {
Authorization: `Bearer ${result.token}`,
},
},
});
state.auth =
await state.api.get(
"/auth",
);
router.push("/dashboard");
}
if (res == "denied") {
setPageState("denied");
}
setOtp("");
setToken("");
setWs(null);
setUid("");
},
),
);
} catch (e) {
console.error(e);
try {
setError(JSON.stringify(e, null, 4));
} catch (e) {
setError("" + e);
}
setPageState("error");
}
}}
>
Go!
</button>
</>
);
case "loading":
return (
<>
<span>Just one second...</span>
</>
);
case "listening":
return (
<>
<h1>Just one more step</h1>
<p>
To finish, run the following in AutoMod's direct
messages: <code>/login {otp}</code>
</p>
</>
);
case "error":
return (
<>
<h1>Hm, something went wrong.</h1>
<p>{error}</p>
</>
);
case "denied":
return (
<>
<h1>Authentication request was denied.</h1>
<button onClick={() => setPageState("input")}>
Try again?
</button>
</>
);
}
}