Skip to content

Commit

Permalink
Merge pull request #19 from UniCourse-TW/feat-setup-guide
Browse files Browse the repository at this point in the history
Setup Guide in GUI
  • Loading branch information
JacobLinCool authored Mar 25, 2023
2 parents 13b99ce + 5b740cf commit 70b8f6e
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 17 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ NEO4J_PASSWORD="password"
NEO4J_AUTH="${NEO4J_USER}/${NEO4J_PASSWORD}"
JWT_SECRET="unicourse-jwt-secret"
CLOUDFLARED_TOKEN=""
UNICOURSE_FIRST_INVITATION_CODE="first_code"
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,24 @@ A POC for Hybrid UniCourse Server.

## First Run

> You'll need to create the first invitation to invite yourself.
1. Open a terminal in VSCode.
2. Run `pnpm dev` to start the server in development mode.
3. Go to `http://localhost:3000/`, you should see there are 0 courses, 0 posts, and 1 user.
4. Go to `http://localhost:7474/`, which is the database management interface.
5. Login as `neo4j` with password `password`.
6. Run the following query to create an invitation:
3. Go to `http://localhost:5173/`, you should see there are 0 courses, 0 posts, and 1 user.
4. You should also see a panel with title "Welcome aboard", this panel only appears for your first account (specifically, if these only 1 user in DB).
5. Get started with entering usernames and roles, then you would be navigated to register page to complete the account information.
6. Voilà, your first account is ready!

```cypher
MATCH (x:User {username: "admin"}) MERGE (x)<-[:OWNED_BY]-(invitation:Invitation { code: "first_code", created: datetime(), revoked: false })
```
In case the above approach doesn't work, you can follow these instructions to create your account in DB.

7. Then, you can use the invitation code to register your account.
8. Next, you can give yourself roles `Verified`, `CoursePacker`, `Moderator` by running queries like:
1. Go to `http://localhost:7474/`, which is the database management interface.
2. Login as `neo4j` with password `password`.
3. In the browser view, click on the `*` node labels to see the data in graph and see the invitation code owned by admin.
4. Then, you can use the invitation code to register your account in the realm website.
5. Next, you can give yourself roles `Verified`, `CoursePacker`, `Moderator` by running queries like:

```cypher
MATCH (u:User {username: "your_username"}) SET u:Verified
```

9. Remember to re-login to refresh the token.
10. Now you can import some course packs and start using UniCourse Realm!
6. Remember to re-login to refresh the token.
7. Now you can import some course packs and start using UniCourse Realm!
12 changes: 12 additions & 0 deletions src/lib/actions/Auth.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import { invalidateAll } from "$app/navigation";
import type { RoleType } from "$lib/constants";
import { onMount } from "svelte";
import { t } from "svelte-i18n";
import { hash } from "unicourse";
Expand All @@ -12,12 +14,21 @@
let password_confirm = "";
let email = "";
let invitation = "";
let roles: RoleType[] = [];
$: {
console.log("switched to mode: " + mode);
err = "";
}
onMount(() => {
const params = new URLSearchParams(location.search);
if (params.has("username")) username = params.get("username")!;
if (params.has("roles")) roles = params.get("roles")!.split(",") as RoleType[];
if (params.has("code")) invitation = params.get("code")!;
if (roles || invitation) mode = "register";
});
async function login() {
if (username === "" || password === "") {
err = $t("auth.please-fill-in-username-and-password");
Expand Down Expand Up @@ -78,6 +89,7 @@
password: await hash(password),
email,
invitation,
roles,
}),
});
Expand Down
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export const Role = {
Moderator: "Moderator",
CoursePacker: "CoursePacker",
} as const;

export type RoleType = (typeof Role)[keyof typeof Role];
1 change: 1 addition & 0 deletions src/lib/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const NEO4J_URI = process.env.NEO4J_URI || "neo4j://db:7687";
export const NEO4J_USER = process.env.NEO4J_USER || "neo4j";
export const NEO4J_PASSWORD = process.env.NEO4J_PASSWORD || "password";
export const JWT_SECRET = process.env.JWT_SECRET || "unicourse-jwt-secret";
export const FIRST_INVITATION_CODE = process.env.UNICOURSE_FIRST_INVITATION_CODE || "first_code";
14 changes: 13 additions & 1 deletion src/lib/server/db.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { building } from "$app/environment";
import neo4j from "neo4j-driver";
import { DB } from "neo4j-ogm";
import { NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD } from "./config";
import { NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD, FIRST_INVITATION_CODE } from "./config";
import { constraint } from "./db-utils";

const driver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USER, NEO4J_PASSWORD));
Expand Down Expand Up @@ -53,5 +53,17 @@ export const ready = (async () => {
`,
);

await db.run(
`
MATCH (x:User {username: "admin"})
MERGE (x)<-[:OWNED_BY]-(invitation:Invitation {
code: $invitation_code
})
ON CREATE SET invitation.created = datetime(),
invitation.revoked = false
`,
{ invitation_code: FIRST_INVITATION_CODE },
);

console.timeEnd("Database Ready");
})();
2 changes: 2 additions & 0 deletions src/lib/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const en = {
invitation_invalid: "Invitation is invalid",
wrong_password: "Wrong password",
wrong_invitation: "Wrong invitation code",
wrong_roles: "Wrong roles",
register_with_roles: "You can't register with roles",
not_logged_in: "Not logged in",
permission_denied: "Permission denied",
},
Expand Down
3 changes: 2 additions & 1 deletion src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FIRST_INVITATION_CODE } from "$lib/server/config";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ fetch }) => {
const response = await fetch("/api/stats");
const { data: stats } = await response.json();
return { stats };
return { stats, code: FIRST_INVITATION_CODE };
};
61 changes: 61 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { Role } from "$lib/constants";
import type { RoleType } from "$lib/constants";
import { t } from "svelte-i18n";
import { fly, fade } from "svelte/transition";
import type { PageData } from "./$types";
Expand All @@ -11,10 +14,68 @@
}
const start = 300;
const roleOptions: RoleType[] = [Role.Verified, Role.CoursePacker, Role.Moderator];
let username = "";
let roles: RoleType[] = [];
$: isComplete = username.length > 0 && roles.length > 0;
function getStarted() {
if (isComplete) {
goto(`/auth?username=${username}&roles=${roles.join(",")}&code=${data.code}`);
}
}
</script>

<section class="h-full pt-12">
<div class="flex h-full w-full flex-col justify-center">
{#if data.stats.users == 1}
<div in:fly={{ y: -40, delay: start + 2000, duration: 800 }} class="mb-20">
<h1 in:fade={{ duration: 300 }} class="mb-4 text-5xl font-bold">
Welcome aboard, developer!
</h1>
<h2
in:fade={{ delay: start + 2100, duration: 300 }}
class="mb-4 pl-2 font-medium text-primary/90"
>
Register your very first account and initiate UniCourse Realm.
</h2>
<div in:fade={{ delay: start + 2300, duration: 300 }} class="input-group">
<input
placeholder="Enter Username"
class="input bg-white/70 placeholder:text-sm placeholder:font-semibold placeholder:text-gray-500 focus:outline-none"
bind:value={username}
/>
<span class="bg-white/70 text-sm font-semibold lowercase text-primary">as</span>
<div class="dropdown-start dropdown-left dropdown">
<button
tabindex="0"
class="btn-ghost btn rounded-none bg-white/70 normal-case focus:outline-none"
class:text-gray-500={roles.length === 0}
>{roles.join(", ") || "Select Roles"}</button
>
<button tabindex="0" class="dropdown-content rounded-box w-48 bg-white p-4">
{#each roleOptions as role}
<label class="label cursor-pointer">
<span class="label-text bg-transparent">{role}</span>
<input
type="checkbox"
class="checkbox"
bind:group={roles}
value={role}
/>
</label>
{/each}
</button>
</div>
<button
class="btn-primary btn"
class:btn-disabled={!isComplete}
on:click={getStarted}>Get Started</button
>
</div>
</div>
{/if}

<div class="mb-10">
<h1 in:fly={{ y: 40, delay: start, duration: 500 }} class="text-4xl">UniCourse</h1>
<h1 in:fly={{ y: -20, delay: start + 500, duration: 600 }} class="text-8xl font-bold">
Expand Down
33 changes: 31 additions & 2 deletions src/routes/api/auth/register/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { v } from "@unicourse-tw/validation";
export const POST: RequestHandler = async ({ request, cookies }) => {
await ready;

let { username, password, email, invitation } = await request.json();
let { username, password, email, invitation, roles } = await request.json();

try {
username = v.username.parse(username);
Expand All @@ -40,11 +40,39 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
return json({ error: en.auth.invitation_invalid }, { status: 400 });
}

if (roles) {
try {
roles = z
.array(
z.union([
z.literal("Verified"),
z.literal("CoursePacker"),
z.literal("Moderator"),
]),
)
.parse(roles);
roles.unshift("User");
} catch {
return json({ error: en.auth.wrong_roles }, { status: 400 });
}
try {
const { records } = await db.run(
`MATCH (u:User) RETURN count(u) > 1 AS hasMoreThanOneUser`,
);
const hasMoreThanOneUser = records[0].get("hasMoreThanOneUser");
if (hasMoreThanOneUser) throw new Error();
} catch {
return json({ error: en.auth.register_with_roles }, { status: 403 });
}
} else {
roles = ["User"];
}

const payload = {
id: createId(),
username: username,
expires: Date.now() + 1000 * 60 * 60,
roles: ["User"],
roles,
};

const jwt = JWT.sign(payload, JWT_SECRET, {
Expand All @@ -62,6 +90,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
WITH user, owner
CREATE (user)-[:FOLLOWS]->(owner)
CREATE (token:Token $token)-[:OWNED_BY]->(user)
SET ${roles.map((x: string) => `user:${x}`).join(", ")}
RETURN user, owner.username as referrer
`,
{
Expand Down

0 comments on commit 70b8f6e

Please sign in to comment.