return user profile when fetching auth

master
Lea 2023-07-24 21:19:12 +02:00
parent 5884e90d96
commit 592f1c5b43
19 changed files with 293 additions and 106 deletions

View File

@ -1,9 +1,7 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiOperation, ApiProperty, ApiResponse } from '@nestjs/swagger';
import { ApiOperation, ApiProperty } from '@nestjs/swagger';
import { META } from './meta';
import { Auth, AuthData } from './auth/authdata.decorator';
import { AuthenticationData } from './auth/auth.guard';
import { ApiUserInfo } from 'lib';
export class ApiInfo {
@ApiProperty()
@ -19,12 +17,13 @@ export class AuthInfo {
@ApiProperty({ default: false })
god: boolean;
@ApiProperty({ type: () => ApiUserInfo, nullable: true })
profile: ApiUserInfo | null;
}
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({ summary: 'Fetch API configuration' })
getHello(): ApiInfo {
@ -32,17 +31,4 @@ export class AppController {
version: META.version,
};
}
@Get('/auth')
@Auth()
@ApiOperation({ summary: 'Check authentication status' })
@ApiResponse({ type: AuthInfo, status: 200 })
getAuth(@AuthData() auth: AuthenticationData): AuthInfo {
return {
// Since the AuthGuard stops the request otherwise, we can assume the user is authenticated
authenticated: true,
user: auth.user,
god: auth.god || false,
};
}
}

View File

@ -1,7 +1,15 @@
import { Body, Controller, Ip, Post } from '@nestjs/common';
import { ApiProperty, ApiResponse, ApiTags } from '@nestjs/swagger';
import { NoAuthentication } from './authdata.decorator';
import { Body, Controller, Get, Ip, Post } from '@nestjs/common';
import {
ApiOperation,
ApiProperty,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { Auth, AuthData, NoAuthentication } from './authdata.decorator';
import { AuthService } from './auth.service';
import { AuthInfo } from 'src/app.controller';
import { AuthenticationData } from './auth.guard';
import { DatabaseService } from 'src/database/database.service';
export class LoginData {
@ApiProperty({ description: 'The user ID you are trying to log in as' })
@ -18,7 +26,23 @@ export class LoginResponse {
@Controller('auth')
@ApiTags('authentication')
export class AuthController {
constructor(private auth: AuthService) {}
constructor(private auth: AuthService, private db: DatabaseService) {}
@Get('/')
@Auth()
@ApiOperation({ summary: 'Check authentication status' })
@ApiResponse({ type: AuthInfo, status: 200 })
async getAuth(@AuthData() auth: AuthenticationData): Promise<AuthInfo> {
return {
// Since the AuthGuard stops the request otherwise, we can assume the user is authenticated
authenticated: true,
user: auth.user,
god: auth.god || false,
profile: auth.user
? (await this.db.getMessengers().getuserInfo({ id: auth.user })) || null
: null,
};
}
@Post('login')
@NoAuthentication()

View File

@ -13,6 +13,8 @@ import {
GetMutualServersRes,
GetServerDetailsReq,
GetServerDetailsRes,
UserInfoReq,
UserInfoRes,
connectDb,
connectRedis,
createRedisIPCSender,
@ -54,6 +56,11 @@ async function createMessengers(redis: AutomodRedis, rcvRedis: AutomodRedis) {
GetServerDetailsReq,
GetServerDetailsRes
>('getServerDetails', redis, rcvRedis),
getuserInfo: createRedisIPCSender<UserInfoReq, UserInfoRes>(
'getUserInfo',
redis,
rcvRedis,
),
};
}

View File

@ -13,6 +13,8 @@ import {
InfractionType,
ModActionType,
ServerLogCategory,
UserInfoReq,
UserInfoRes,
canModifyServerSettings,
connectRedis,
createInfraction,
@ -143,6 +145,24 @@ redisIPCHandler<GetServerDetailsReq, GetServerDetailsRes>(
},
);
redisIPCHandler<UserInfoReq, UserInfoRes | null>(
"getUserInfo",
client.redis,
rcvRedis,
async (msg) => {
const user = await fetchUser(msg.id, client);
return user
? {
id: user.id,
username: user.username,
discriminator: user.discriminator,
displayName: user.displayName,
avatarUrl: user.avatarURL,
}
: null;
},
);
export const authRequestsSender = createRedisIPCSender<
AuthRequestInteractionReq,
AuthRequestInteractionRes

View File

@ -11,6 +11,7 @@
"author": "",
"license": "AGPL-3.0",
"dependencies": {
"@nestjs/swagger": "^7.0.12",
"log75": "^3.0.1",
"mongodb": "^5.6.0",
"redis": "^4.6.7",

View File

@ -23,5 +23,6 @@ export * from "./types/redis/create_infraction.js";
export * from "./types/redis/get_mutual_servers.js";
export * from "./types/redis/get_server_details.js";
export * from "./types/redis/auth_request.js";
export * from "./types/redis/user_info.js";
export const AUTH_TOKEN_LIFETIME = 1000 * 60 * 60 * 24;

View File

@ -0,0 +1,24 @@
import { ApiProperty } from "@nestjs/swagger";
export class ApiUserInfo {
@ApiProperty({ type: String })
id: string;
@ApiProperty({ type: String })
username: string;
@ApiProperty({ type: String })
discriminator: string;
@ApiProperty({ type: String, nullable: true })
displayName?: string;
@ApiProperty({ type: String, nullable: true })
avatarUrl?: string;
}
export type UserInfoReq = {
id: string;
};
export type UserInfoRes = ApiUserInfo | null;

View File

@ -11,6 +11,8 @@
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"strictPropertyInitialization": false
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"skipLibCheck": true
}
}

View File

@ -13,36 +13,6 @@
}
}
},
"/auth": {
"get": {
"operationId": "getAuth",
"summary": "Check authentication status",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthInfo"
}
}
}
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"security": [
{
"bearer": []
}
]
}
},
"/servers/{server}/infractions": {
"get": {
"operationId": "listInfractions",
@ -281,6 +251,39 @@
]
}
},
"/auth": {
"get": {
"operationId": "getAuth",
"summary": "Check authentication status",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthInfo"
}
}
}
},
"401": {
"description": "No valid authentication presented"
},
"403": {
"description": "The user does not have access to the resource"
}
},
"tags": [
"authentication"
],
"security": [
{
"bearer": []
}
]
}
},
"/auth/login": {
"post": {
"operationId": "login",
@ -416,26 +419,6 @@
}
},
"schemas": {
"AuthInfo": {
"type": "object",
"properties": {
"authenticated": {
"type": "boolean"
},
"user": {
"type": "string"
},
"god": {
"type": "boolean",
"default": false
}
},
"required": [
"authenticated",
"user",
"god"
]
},
"CreateInfractionData": {
"type": "object",
"properties": {
@ -554,6 +537,64 @@
"config"
]
},
"ApiUserInfo": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"username": {
"type": "string"
},
"discriminator": {
"type": "string"
},
"displayName": {
"type": "string",
"nullable": true
},
"avatarUrl": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"username",
"discriminator",
"displayName",
"avatarUrl"
]
},
"AuthInfo": {
"type": "object",
"properties": {
"authenticated": {
"type": "boolean"
},
"user": {
"type": "string"
},
"god": {
"type": "boolean",
"default": false
},
"profile": {
"nullable": true,
"allOf": [
{
"$ref": "#/components/schemas/ApiUserInfo"
}
]
}
},
"required": [
"authenticated",
"user",
"god",
"profile"
]
},
"LoginData": {
"type": "object",
"properties": {

View File

@ -1,3 +1,3 @@
// This file was auto-generated by @insertish/oapi!
export const pathResolve = {"1":[[""],["auth"],["servers"],["test"]],"2":[["servers",["{server}"]],["auth","login"]],"3":[["servers",["{server}"],"infractions"]],"4":[["servers",["{server}"],"infractions",["{id}"]]]};
export const queryParams = {"/":{"get":[]},"/auth":{"get":[]},"/servers/{server}/infractions":{"get":["user","mod","limit","before","after"],"post":[]},"/servers/{server}/infractions/{id}":{"get":[]},"/servers":{"get":[]},"/servers/{server}":{"get":[]},"/auth/login":{"post":[]},"/test":{"get":[],"post":[],"patch":[],"put":[],"delete":[]}};
export const pathResolve = {"1":[[""],["servers"],["auth"],["test"]],"2":[["servers",["{server}"]],["auth","login"]],"3":[["servers",["{server}"],"infractions"]],"4":[["servers",["{server}"],"infractions",["{id}"]]]};
export const queryParams = {"/":{"get":[]},"/servers/{server}/infractions":{"get":["user","mod","limit","before","after"],"post":[]},"/servers/{server}/infractions/{id}":{"get":[]},"/servers":{"get":[]},"/servers/{server}":{"get":[]},"/auth":{"get":[]},"/auth/login":{"post":[]},"/test":{"get":[],"post":[],"patch":[],"put":[],"delete":[]}};

View File

@ -2,7 +2,6 @@
import { paths } from './schema';
export type APIRoutes =
| { method: 'get', path: `/`, parts: 1, params: undefined, response: undefined }
| { method: 'get', path: `/auth`, parts: 1, params: undefined, response: paths['/auth']['get']['responses']['200']['content']['application/json'] }
| { method: 'get', path: `/servers/${string}/infractions`, parts: 3, params: paths['/servers/{server}/infractions']['get']['parameters']['query'], response: undefined }
| { method: 'get', path: '-/servers/{server}/infractions', parts: 3, params: paths['/servers/{server}/infractions']['get']['parameters']['query'], response: undefined }
| { method: 'post', path: `/servers/${string}/infractions`, parts: 3, params: paths['/servers/{server}/infractions']['post']['requestBody']['content']['application/json'], response: undefined }
@ -12,6 +11,7 @@ export type APIRoutes =
| { 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: `/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: 'get', path: `/test`, parts: 1, params: undefined, response: undefined }
| { method: 'post', path: `/test`, parts: 1, params: undefined, response: undefined }

View File

@ -9,10 +9,6 @@ export interface paths {
/** Fetch API configuration */
get: operations["getHello"];
};
"/auth": {
/** Check authentication status */
get: operations["getAuth"];
};
"/servers/{server}/infractions": {
/** Fetch a list of infractions */
get: operations["listInfractions"];
@ -31,6 +27,10 @@ export interface paths {
/** Fetch details about a specific server */
get: operations["getServer"];
};
"/auth": {
/** Check authentication status */
get: operations["getAuth"];
};
"/auth/login": {
post: operations["login"];
};
@ -47,12 +47,6 @@ export type webhooks = Record<string, never>;
export interface components {
schemas: {
AuthInfo: {
authenticated: boolean;
user: string;
/** @default false */
god: boolean;
};
CreateInfractionData: {
user: string;
reason: string;
@ -84,6 +78,20 @@ export interface components {
info: components["schemas"]["ServerInfo"];
config: components["schemas"]["ServerConfig"];
};
ApiUserInfo: {
id: string;
username: string;
discriminator: string;
displayName: string | null;
avatarUrl: string | null;
};
AuthInfo: {
authenticated: boolean;
user: string;
/** @default false */
god: boolean;
profile: components["schemas"]["ApiUserInfo"] | null;
};
LoginData: {
/** @description The user ID you are trying to log in as */
user: string;
@ -110,20 +118,6 @@ export interface operations {
200: never;
};
};
/** Check authentication status */
getAuth: {
responses: {
200: {
content: {
"application/json": components["schemas"]["AuthInfo"];
};
};
/** @description No valid authentication presented */
401: never;
/** @description The user does not have access to the resource */
403: never;
};
};
/** Fetch a list of infractions */
listInfractions: {
parameters: {
@ -217,6 +211,20 @@ export interface operations {
};
};
};
/** Check authentication status */
getAuth: {
responses: {
200: {
content: {
"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: {
requestBody: {
content: {

View File

@ -1,11 +1,12 @@
// This file was auto-generated by @insertish/oapi!
import { components } from './schema';
export type AuthInfo = components['schemas']['AuthInfo'];
export type CreateInfractionData = components['schemas']['CreateInfractionData'];
export type ServerInfo = components['schemas']['ServerInfo'];
export type GetServersResponse = components['schemas']['GetServersResponse'];
export type ServerLogsConfig = components['schemas']['ServerLogsConfig'];
export type ServerConfig = components['schemas']['ServerConfig'];
export type ServerDetails = components['schemas']['ServerDetails'];
export type ApiUserInfo = components['schemas']['ApiUserInfo'];
export type AuthInfo = components['schemas']['AuthInfo'];
export type LoginData = components['schemas']['LoginData'];
export type LoginResponse = components['schemas']['LoginResponse'];;

View File

@ -283,6 +283,9 @@ importers:
lib:
dependencies:
'@nestjs/swagger':
specifier: ^7.0.12
version: 7.0.12(@nestjs/common@10.0.0)(@nestjs/core@10.0.0)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)
log75:
specifier: ^3.0.1
version: 3.0.1
@ -366,6 +369,9 @@ importers:
lib:
specifier: workspace:*
version: link:../lib
localforage:
specifier: ^1.10.0
version: 1.10.0
next:
specifier: 13.4.10
version: 13.4.10(react-dom@18.2.0)(react@18.2.0)
@ -4636,6 +4642,10 @@ packages:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
/immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@ -5561,6 +5571,12 @@ packages:
/libphonenumber-js@1.10.37:
resolution: {integrity: sha512-Z10PCaOCiAxbUxLyR31DNeeNugSVP6iv/m7UrSKS5JHziEMApJtgku4e9Q69pzzSC9LnQiM09sqsGf2ticZnMw==}
/lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
dependencies:
immediate: 3.0.6
dev: false
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@ -5574,6 +5590,12 @@ packages:
engines: {node: '>=6.11.5'}
dev: true
/localforage@1.10.0:
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
dependencies:
lie: 3.1.1
dev: false
/locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}

View File

@ -16,6 +16,7 @@
"eslint": "8.45.0",
"eslint-config-next": "13.4.10",
"lib": "workspace:*",
"localforage": "^1.10.0",
"next": "13.4.10",
"oapilib": "workspace:*",
"postcss": "8.4.26",

32
web/src/app/auth/page.tsx Normal file
View File

@ -0,0 +1,32 @@
"use client";
import { useBearStore } from "@/components/state";
import { API, AuthInfo } from "oapilib";
import { useEffect, useState } from "react";
export default function Auth() {
const state = useBearStore();
const [authInfo, setAuthInfo] = useState<AuthInfo | undefined>(state.auth);
useEffect(() => {
state.api = new API({
authentication: { headers: { Authorization: "Bearer banana" } },
});
if (!authInfo) {
state.api
.get("/auth")
.then((res) => setAuthInfo(res))
.catch((e) => alert(e));
}
});
return authInfo ? (
<div>
<h1>hi</h1>
<p>{authInfo.profile?.displayName}</p>
</div>
) : (
<span>real</span>
);
}

View File

@ -1,3 +1,4 @@
import { useBearStore } from "@/components/state";
import "./globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
@ -5,11 +6,11 @@ import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "AutoMod",
description: "Revolt's most hated moderation bot",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;

View File

@ -1,8 +1,9 @@
import { API } from "oapilib";
import { API, AuthInfo } from "oapilib";
import { create } from "zustand";
type State = {
api: API;
auth?: AuthInfo;
};
export const useBearStore = create<State>((set) => ({

15
web/src/lib/auth.ts Normal file
View File

@ -0,0 +1,15 @@
import localforage from "localforage";
type CredentialsData = {
token: string | null;
};
export async function setCredentials({ token }: CredentialsData) {
await localforage.setItem("api_token", token);
}
export async function getCredentials(): Promise<CredentialsData> {
return {
token: await localforage.getItem("api_token"),
};
}