Login with extension

This commit is contained in:
Admin 2023-05-06 00:01:27 -04:00
parent 093ff3af56
commit d43ffb680a
30 changed files with 5378 additions and 2476 deletions

View File

@ -1,8 +1,8 @@
{
"tabWidth": 2,
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"pluginSearchDirs": ["."]
"tabWidth": 2,
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"pluginSearchDirs": ["."]
}

View File

@ -1,6 +1,4 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint"
]

View File

@ -1,7 +1,3 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
@ -15,20 +11,6 @@
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
]
}

View File

@ -1,11 +1,9 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
"out": false
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
"out": true
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

View File

@ -1,12 +1,9 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"

View File

@ -1,9 +0,0 @@
# Change Log
All notable changes to the "bwcontest" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -0,0 +1,30 @@
html {
box-sizing: border-box;
font-size: 13px;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
img {
max-width: 100%;
height: auto;
}

View File

@ -0,0 +1,91 @@
:root {
--container-padding: 20px;
--input-padding-vertical: 6px;
--input-padding-horizontal: 4px;
--input-margin-vertical: 4px;
--input-margin-horizontal: 0;
}
body {
padding: 0 var(--container-padding);
color: var(--vscode-foreground);
font-size: var(--vscode-font-size);
font-weight: var(--vscode-font-weight);
font-family: var(--vscode-font-family);
background-color: var(--vscode-editor-background);
}
ol,
ul {
padding-left: var(--container-padding);
}
body > *,
form > * {
margin-block-start: var(--input-margin-vertical);
margin-block-end: var(--input-margin-vertical);
}
*:focus {
outline-color: var(--vscode-focusBorder) !important;
}
a {
color: var(--vscode-textLink-foreground);
}
a:hover,
a:active {
color: var(--vscode-textLink-activeForeground);
}
code {
font-size: var(--vscode-editor-font-size);
font-family: var(--vscode-editor-font-family);
}
button {
border: none;
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
width: 100%;
text-align: center;
outline: 1px solid transparent;
outline-offset: 2px !important;
color: var(--vscode-button-foreground);
background: var(--vscode-button-background);
}
button:hover {
cursor: pointer;
background: var(--vscode-button-hoverBackground);
}
button:focus {
outline-color: var(--vscode-focusBorder);
}
button.secondary {
color: var(--vscode-button-secondaryForeground);
background: var(--vscode-button-secondaryBackground);
}
button.secondary:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
input:not([type='checkbox']),
textarea {
display: block;
width: 100%;
border: none;
font-family: var(--vscode-font-family);
padding: var(--input-padding-vertical) var(--input-padding-horizontal);
color: var(--vscode-input-foreground);
outline-color: var(--vscode-input-border);
background-color: var(--vscode-input-background);
}
input::placeholder,
textarea::placeholder {
color: var(--vscode-input-placeholderForeground);
}

File diff suppressed because it is too large Load Diff

View File

@ -12,36 +12,74 @@
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"viewsContainers": {
"activitybar": [
{
"id": "bwcontest-sidebar-view",
"title": "BWContest",
"icon": "media/icon.png"
}
]
},
"views": {
"bwcontest-sidebar-view": [
{
"type": "webview",
"id": "bwcontest-sidebar",
"name": "BWContest",
"icon": "media/icon.png",
"contextualTitle": "BWContest"
}
]
},
"commands": [
{
"command": "bwcontest.helloWorld",
"category": "BWContest",
"title": "Hello World"
},
{
"command": "bwcontest.testThing",
"title": "Test Thing"
"command": "bwcontest.askQuestion",
"category": "BWContest",
"title": "Ask Question"
}
]
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"compile": "rollup -c && tsc -p ./",
"watch": "concurrently \"rollup -c -w\" \"tsc -watch -p ./\"",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/vscode": "^1.78.0",
"@rollup/plugin-commonjs": "^24.1.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-typescript": "^11.1.0",
"@tsconfig/svelte": "^4.0.1",
"@types/glob": "^8.1.0",
"@types/mocha": "^10.0.1",
"@types/node": "16.x",
"@types/vscode": "^1.78.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"@vscode/test-electron": "^2.3.0",
"concurrently": "^8.0.1",
"eslint": "^8.39.0",
"glob": "^8.1.0",
"mocha": "^10.2.0",
"typescript": "^5.0.4",
"@vscode/test-electron": "^2.3.0"
"postcss": "^8.4.23",
"rollup-plugin-css-only": "^4.3.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-svelte": "^7.1.4",
"rollup-plugin-terser": "^7.0.2",
"svelte": "^3.59.0",
"svelte-check": "^3.3.1",
"svelte-preprocess": "^5.0.3",
"typescript": "^5.0.4"
},
"dependencies": {
"axios": "^1.4.0"
}
}

View File

@ -0,0 +1,49 @@
import svelte from "rollup-plugin-svelte";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import { terser } from "rollup-plugin-terser";
import sveltePreprocess from "svelte-preprocess";
import typescript from "@rollup/plugin-typescript";
import path from "path";
import fs from "fs";
import css from "rollup-plugin-css-only";
const production = !process.env.ROLLUP_WATCH;
export default fs
.readdirSync(path.join(__dirname, "webviews", "pages"))
.map((input) => {
const name = input.split(".")[0];
return {
input: "webviews/pages/" + input,
output: {
sourcemap: true,
format: "iife",
name: "app",
file: "out/compiled/" + name + ".js",
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
preprocess: sveltePreprocess(),
emitCss: true
}),
css({ output: name + ".css" }),
resolve({
browser: true,
dedupe: ["svelte"],
}),
commonjs(),
typescript({
tsconfig: "webviews/tsconfig.json",
sourceMap: !production,
inlineSources: !production,
}),
production && terser(),
],
watch: {
clearScreen: false,
},
};
});

View File

@ -0,0 +1,164 @@
import * as vscode from 'vscode';
import { getNonce } from './getNonce';
export class BWPanel {
/**
* Track the currently panel. Only allow a single panel to exist at a time.
*/
public static currentPanel: BWPanel | undefined;
public static readonly viewType = 'bwpanel';
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri;
private _disposables: vscode.Disposable[] = [];
public static createOrShow(extensionUri: vscode.Uri) {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
// If we already have a panel, show it.
if (BWPanel.currentPanel) {
BWPanel.currentPanel._panel.reveal(column);
BWPanel.currentPanel._update();
return;
}
// Otherwise, create a new panel.
const panel = vscode.window.createWebviewPanel(
BWPanel.viewType,
'VSinder',
column || vscode.ViewColumn.One,
{
// Enable javascript in the webview
enableScripts: true,
// And restrict the webview to only loading content from our extension's `media` directory.
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, 'media'),
vscode.Uri.joinPath(extensionUri, 'out/compiled')
]
}
);
BWPanel.currentPanel = new BWPanel(panel, extensionUri);
}
public static kill() {
BWPanel.currentPanel?.dispose();
BWPanel.currentPanel = undefined;
}
public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
BWPanel.currentPanel = new BWPanel(panel, extensionUri);
}
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
this._panel = panel;
this._extensionUri = extensionUri;
// Set the webview's initial html content
this._update();
// Listen for when the panel is disposed
// This happens when the user closes the panel or when the panel is closed programatically
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
// // Handle messages from the webview
// this._panel.webview.onDidReceiveMessage(
// (message) => {
// switch (message.command) {
// case "alert":
// vscode.window.showErrorMessage(message.text);
// return;
// }
// },
// null,
// this._disposables
// );
}
public dispose() {
BWPanel.currentPanel = undefined;
// Clean up our resources
this._panel.dispose();
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
}
private async _update() {
const webview = this._panel.webview;
this._panel.webview.html = this._getHtmlForWebview(webview);
webview.onDidReceiveMessage(async (data) => {
switch (data.type) {
case 'onInfo': {
if (!data.value) {
return;
}
vscode.window.showInformationMessage(data.value);
break;
}
case 'onError': {
if (!data.value) {
return;
}
vscode.window.showErrorMessage(data.value);
break;
}
// case "tokens": {
// await Util.globalState.update(accessTokenKey, data.accessToken);
// await Util.globalState.update(refreshTokenKey, data.refreshToken);
// break;
// }
}
});
}
private _getHtmlForWebview(webview: vscode.Webview) {
// // And the uri we use to load this script in the webview
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'out/compiled', 'HelloWorld.js')
);
// Uri to load styles into webview
const stylesResetUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'reset.css')
);
const stylesMainUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'vscode.css')
);
// const cssUri = webview.asWebviewUri(
// vscode.Uri.joinPath(this._extensionUri, 'out', 'compiled/swiper.css')
// );
// // Use a nonce to only allow specific scripts to be run
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--
Use a content security policy to only allow loading images from https or from our extension directory,
and only allow scripts that have a specific nonce.
-->
<meta http-equiv="Content-Security-Policy" content="img-src https: data:; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${stylesResetUri}" rel="stylesheet">
<link href="${stylesMainUri}" rel="stylesheet">
</head>
<body>
</body>
<script src=${scriptUri} nonce="${nonce}">
</script>
</html>`;
}
}

View File

@ -0,0 +1,82 @@
import * as vscode from 'vscode';
import { getNonce } from './getNonce';
export class SidebarProvider implements vscode.WebviewViewProvider {
_view?: vscode.WebviewView;
constructor(private readonly _extensionUri: vscode.Uri) {}
public resolveWebviewView(webviewView: vscode.WebviewView) {
this._view = webviewView;
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
webviewView.webview.onDidReceiveMessage(async (data) => {
switch (data.type) {
case 'onInfo': {
if (!data.value) {
return;
}
vscode.window.showInformationMessage(data.value);
break;
}
case 'onError': {
if (!data.value) {
return;
}
vscode.window.showErrorMessage(data.value);
break;
}
}
});
}
private _getHtmlForWebview(webview: vscode.Webview) {
const styleResetUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'reset.css')
);
const styleVSCodeUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'media', 'vscode.css')
);
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'out', 'compiled/sidebar.js')
);
const styleMainUri = webview.asWebviewUri(
vscode.Uri.joinPath(this._extensionUri, 'out', 'compiled/sidebar.css')
);
// Use a nonce to only allow a specific script to be run.
const nonce = getNonce();
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!--
Use a content security policy to only allow loading images from https or from our extension directory,
and only allow scripts that have a specific nonce.
-->
<meta http-equiv="Content-Security-Policy" content=" img-src https: data:; style-src 'unsafe-inline' ${webview.cspSource}; script-src 'nonce-${nonce}';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${styleResetUri}" rel="stylesheet">
<link href="${styleVSCodeUri}" rel="stylesheet">
<link href="${styleMainUri}" rel="stylesheet">
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
</script>
</head>
<body>
</body>
<script nonce="${nonce}" src="${scriptUri}"></script>
</html>`;
}
}

View File

@ -1,13 +1,29 @@
import * as vscode from 'vscode';
import { BWPanel } from './BWPanel';
import { SidebarProvider } from './SidebarProvider';
import { notDeepEqual } from 'assert';
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, your extension "bwcontest" is now active!');
const sidebarProvider = new SidebarProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider('bwcontest-sidebar', sidebarProvider)
);
let disposable = vscode.commands.registerCommand('bwcontest.helloWorld', () => {
vscode.window.showInformationMessage('Hello World from BWContest!');
});
context.subscriptions.push(
vscode.commands.registerCommand('bwcontest.helloWorld', () => {
})
);
context.subscriptions.push(disposable);
context.subscriptions.push(
vscode.commands.registerCommand('bwcontest.askQuestion', async () => {
const answer = await vscode.window.showInformationMessage('How was your day?', 'good', 'bad');
if (answer === 'bad') {
vscode.window.showInformationMessage('Sorry to hear that');
} else {
console.log(answer);
}
})
);
}
export function deactivate() {}

View File

@ -0,0 +1,8 @@
export function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View File

@ -1,23 +0,0 @@
import * as path from 'path';
import { runTests } from '@vscode/test-electron';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests', err);
process.exit(1);
}
}
main();

View File

@ -1,15 +0,0 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

View File

@ -1,38 +0,0 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -3,15 +3,14 @@
"module": "commonjs",
"target": "ES2020",
"outDir": "out",
"lib": [
"ES2020"
],
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
},
"exclude": ["webviews"]
}

View File

@ -0,0 +1,4 @@
<script lang="ts">
</script>
<h1>Test!</h1>

View File

@ -0,0 +1,61 @@
<script lang="ts">
function postMessage(message: any) {
vscode.postMessage(message);
}
let teamname: HTMLInputElement;
let password: HTMLInputElement;
let sessionToken: string | undefined;
async function onLogin() {
try {
const res = await fetch("http://localhost:5173/api/team/login", {method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
teamname: teamname.value, password: password.value
})});
if (res.status !== 200) {
postMessage({type: 'onError', value: 'Error logging in'});
return;
}
const data = await res.json();
if (data.success === false) {
postMessage({type: 'onError', value: data.message ?? "Unknown error logging in"});
return;
}
sessionToken = data.token;
} catch (err) {
console.error('Failed to fetch:', err);
}
}
async function onLogout() {
const res = await fetch("http://localhost:5173/api/team/logout", {method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
token: sessionToken,
})})
if (res.status !== 200) {
postMessage({type: 'onError', value: 'Error logging out'});
return;
}
const data = await res.json();
if (data.success === true) {
postMessage({type: 'onInfo', value: 'BWContest: Logged out'});
sessionToken = undefined;
} else {
postMessage({type: 'onError', value: 'Log out unsuccessful'});
}
}
</script>
<h1>Contest</h1>
{#if sessionToken === undefined}
<label for="teamname">Team Name</label>
<input bind:this={teamname} id="teamname" type="text"/>
<label for="password">Password</label>
<input bind:this={password} id="password" type="password"/>
<button on:click={onLogin}>Login</button>
{:else}
<button on:click={onLogout}>Logout</button>
{/if}

View File

@ -0,0 +1,9 @@
/// <reference types="svelte" />
type VSCode = {
postMessage(message: any): void;
getState(): any;
setState(state: any): void;
};
declare const vscode: VSCode;

View File

@ -0,0 +1,7 @@
import App from '../components/HelloWorld.svelte';
const app = new App({
target: document.body
});
export default app;

View File

@ -0,0 +1,7 @@
import App from '../components/Sidebar.svelte';
const app = new App({
target: document.body
});
export default app;

View File

@ -0,0 +1,6 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["./**/*"],
"exclude": ["../node_modules/*"],
"compilerOptions": { "strict": true }
}

View File

@ -56,7 +56,16 @@ model Team {
name String @unique
Submission Submission[]
contests Contest[] @relation("TeamContestRelation")
password String
password String
activeTeam ActiveTeam?
}
model ActiveTeam {
id Int @id @default(autoincrement())
teamId Int @unique
team Team @relation(fields: [teamId], references: [id])
sessionToken String? @unique
sessionCreatedAt DateTime?
}
model Contest {

View File

@ -21,6 +21,17 @@ async function removeExpiredSessions(userId: number) {
}
export const handle = (async ({ event, resolve }) => {
if (event.request.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
}
if (event.url.pathname.startsWith('/login')) {
if (event.cookies.get('token')) {
const session = await db.session.findUnique({ where: { token: event.cookies.get('token') } });

View File

@ -0,0 +1,31 @@
import { z } from 'zod';
import type { RequestHandler } from './$types';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/prisma';
import * as UUID from 'uuid';
const loginPostData = z
.object({
teamname: z.string(),
password: z.string()
})
.strict();
export const POST = (async ({ request }) => {
const data = loginPostData.safeParse(await request.json());
if (!data.success) {
throw error(400);
}
const team = await db.team.findUnique({
where: { name: data.data.teamname },
include: { activeTeam: true }
});
if (!team || !team.activeTeam || team.password !== data.data.password) {
return json({ success: false, message: 'Invalid login' });
}
const activeTeam = await db.activeTeam.update({
where: { id: team.activeTeam.id },
data: { sessionToken: UUID.v4(), sessionCreatedAt: new Date() }
});
return json({ success: true, token: activeTeam.sessionToken });
}) satisfies RequestHandler;

View File

@ -0,0 +1,22 @@
import { z } from 'zod';
import type { RequestHandler } from './$types';
import { error, json } from '@sveltejs/kit';
import { db } from '$lib/server/prisma';
const logoutPostData = z
.object({
token: z.string()
})
.strict();
export const POST = (async ({ request }) => {
const data = logoutPostData.safeParse(await request.json());
if (!data.success) {
throw error(400);
}
await db.activeTeam.update({
where: { sessionToken: data.data.token },
data: { sessionToken: null, sessionCreatedAt: null }
});
return json({ success: true });
}) satisfies RequestHandler;