This commit is contained in:
Ondřej Nývlt 2022-12-22 19:49:43 +01:00
parent a4c207ba46
commit 1949587871
7 changed files with 164 additions and 147 deletions

View file

@ -1,6 +1,6 @@
import * as D from "io-ts/Decoder"
import { APP_URL, CLIENT_ID, CLIENT_SECRET } from "../env"
import { expectJson } from "./helpers"
import { expectJson } from "../api/helpers"
const API_URL = "https://discord.com/api/v10"
const REDIRECT_URL = `${APP_URL}/authorize`

72
src/auth/auth.handlers.ts Normal file
View file

@ -0,0 +1,72 @@
import { AUTH_URL, fetchToken, verifyToken } from "./auth.api"
import { HttpError } from "../api/helpers"
import type { RequestHandler } from "express"
import path from "path"
// -----------------------------------------------------------------------------
// Middleware
// -----------------------------------------------------------------------------
export const verifyUser: RequestHandler = async (req, res, next) => {
const token = req.cookies.token
if (!token) {
res.redirect("/login")
return
}
try {
const { user } = await verifyToken(token)
;(req as any).user = user
next()
} catch (error) {
if (error instanceof HttpError && error.status === 401) {
res.redirect("/login")
return
}
res
.status(500)
.send(error instanceof Error ? error.message : "Something went wrong")
}
}
// -----------------------------------------------------------------------------
// Endpoints
// -----------------------------------------------------------------------------
export const getLogin: RequestHandler = async (req, res) => {
const token = req.cookies.token
if (token) {
res.redirect("/members")
}
res.render(path.join(__dirname, "login.view.ejs"), {
layout: path.join(process.cwd(), "src/views/_layout"),
authUrl: AUTH_URL,
})
}
export const authorize: RequestHandler = async (req, res) => {
const code = req.query.code
if (typeof code !== "string") {
throw new Error("Invalid code")
}
try {
const tokenResponse = await fetchToken(code)
res.cookie("token", tokenResponse.access_token)
res.redirect("/members")
} catch (e) {
res
.status(500)
.send(e instanceof Error ? e.message : "Something went wrong")
}
}
export const logout: RequestHandler = (req, res) => {
res.clearCookie("token")
res.redirect("/login")
}

View file

@ -4,7 +4,7 @@
<h1 class="text-2xl font-medium">Univerzity za klima</h1>
<p class="text-lg font-medium mb-12">Členstvo</p>
<a href="<%= authUrl %>" class="bg-discord-default text-white px-6 py-4 rounded hover:bg-discord-active transition-colors">
<%- include("./_discord.svg.ejs", { classes: "inline w-6 mr-2 -ml-1" }) %>
<%- include("../views/_discord.svg.ejs", { classes: "inline w-6 mr-2 -ml-1" }) %>
<span>Přihlásit se přes Discord</span>
</a>
</div>

View file

@ -1,6 +1,3 @@
import * as dotenv from "dotenv"
dotenv.config()
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
import * as D from "io-ts/Decoder"

View file

@ -1,64 +1,14 @@
import { BOT_TOKEN, GUILD_ID, PORT } from "./env"
import * as dotenv from "dotenv"
dotenv.config()
import { PORT } from "./env"
import { Client, Events, GatewayIntentBits } from "discord.js"
import cookieParser from "cookie-parser"
import express from "express"
import path from "path"
import { AUTH_URL, fetchToken, verifyToken } from "./api/auth"
import { HttpError } from "./api/helpers"
import layouts from "express-ejs-layouts"
import type { RequestHandler } from "express"
import type { Role } from "discord.js"
import type { User } from "./api/auth"
// -----------------------------------------------------------------------------
// Model
// -----------------------------------------------------------------------------
let resolveResource: null | ((roles: Array<Role>) => void) = null
const resource: Promise<Array<Role>> = new Promise((resolve, reject) => {
resolveResource = resolve
})
const processRoles = (roles: Array<Role>) => {
return roles
.filter((role) => role.name !== "@everyone")
.map((role) => ({
name: role.name,
color: role.hexColor,
members: role.members.map((member) => ({
nickname: member.nickname,
username: member.user.username,
discriminator: member.user.discriminator,
})),
}))
}
const getAvatarUrl = (user: User) =>
`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
// -----------------------------------------------------------------------------
// Discord client
// -----------------------------------------------------------------------------
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
client.once(Events.ClientReady, async (c) => {
console.log(`Ready! Logged in as ${c.user.tag}`)
const guild = await c.guilds.fetch(GUILD_ID)
const [_, rolesCollection] = await Promise.all([
guild.members.fetch(),
guild.roles.fetch(),
])
resolveResource?.(rolesCollection.toJSON())
console.log("Roles fetched")
})
client.login(BOT_TOKEN)
import { authorize, getLogin, logout, verifyUser } from "./auth/auth.handlers"
import { getMembers } from "./members/members.handlers"
// -----------------------------------------------------------------------------
// Server
@ -68,102 +18,27 @@ const app = express()
app.use(cookieParser())
app.use(layouts)
app.set("views", path.join(__dirname, "views"))
app.set("view engine", "ejs")
const verifyUser: RequestHandler = async (req, res, next) => {
const token = req.cookies.token
if (!token) {
res.redirect(AUTH_URL)
return
}
try {
const { user } = await verifyToken(token)
;(req as any).user = user
next()
} catch (error) {
if (error instanceof HttpError && error.status === 401) {
res.redirect(AUTH_URL)
return
}
res
.status(500)
.send(error instanceof Error ? error.message : "Something went wrong")
}
}
app.use("/", express.static("static"))
app.get("/", async (req, res) => {
const token = req.cookies.token
if (token) {
app.get("/", (req, res) => {
if (!req.cookies.token) {
res.redirect("/login")
} else {
res.redirect("/members")
}
res.render("index", {
layout: path.join(__dirname, "views/_layout"),
authUrl: AUTH_URL,
})
})
app.get("/authorize", async (req, res) => {
const code = req.query.code
// Auth
app.get("/login", getLogin)
app.get("/authorize", authorize)
app.post("/logout", logout)
if (typeof code !== "string") {
throw new Error("Invalid code")
}
try {
const tokenResponse = await fetchToken(code)
res.cookie("token", tokenResponse.access_token)
res.redirect("/members")
} catch (e) {
res
.status(500)
.send(e instanceof Error ? e.message : "Something went wrong")
}
})
app.get("/members.json", verifyUser, async (req, res) => {
try {
const roles = await resource
res.json(processRoles(roles))
} catch (e) {
res
.status(500)
.send(e instanceof Error ? e.message : "Something went wrong")
}
})
app.get("/members", verifyUser, async (req, res) => {
try {
const roles = await resource
const user: User = (req as any).user
res.render("members", {
layout: path.join(__dirname, "views/_layout"),
roles: processRoles(roles),
user: {
...user,
avatarUrl: getAvatarUrl(user),
},
})
} catch (e) {
res
.status(500)
.send(e instanceof Error ? e.message : "Something went wrong")
}
})
app.post("/logout", (req, res) => {
res.clearCookie("token")
res.redirect("/")
})
// Members
app.get("/members", verifyUser, getMembers)
// Listen
app.listen(PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${PORT}`)
})

View file

@ -0,0 +1,73 @@
import { BOT_TOKEN, GUILD_ID } from "../env"
import { Client, Events, GatewayIntentBits } from "discord.js"
import type { RequestHandler } from "express"
import type { Role } from "discord.js"
import type { User } from "../auth/auth.api"
import path from "path"
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
const processRoles = (roles: Array<Role>) => {
return roles
.filter((role) => role.name !== "@everyone")
.map((role) => ({
name: role.name,
color: role.hexColor,
members: role.members.map((member) => ({
nickname: member.nickname,
username: member.user.username,
discriminator: member.user.discriminator,
})),
}))
}
const getAvatarUrl = (user: User) =>
`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
export const getRolesAndMembers = () =>
new Promise<Array<Role>>((resolve) => {
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
client.once(Events.ClientReady, async (c) => {
console.log(`Ready! Logged in as ${c.user.tag}`)
const guild = await c.guilds.fetch(GUILD_ID)
const [_, rolesCollection] = await Promise.all([
guild.members.fetch(),
guild.roles.fetch(),
])
resolve(rolesCollection.toJSON())
console.log("Roles fetched")
})
client.login(BOT_TOKEN)
})
// -----------------------------------------------------------------------------
// Endpoints
// -----------------------------------------------------------------------------
export const getMembers: RequestHandler = async (req, res) => {
try {
const roles = await getRolesAndMembers()
const user: User = (req as any).user
res.render(path.join(__dirname, "members.view.ejs"), {
layout: path.join(process.cwd(), "src/views/_layout"),
roles: processRoles(roles),
user: {
...user,
avatarUrl: getAvatarUrl(user),
},
})
} catch (e) {
res
.status(500)
.send(e instanceof Error ? e.message : "Something went wrong")
}
}