Skip to content
This repository has been archived by the owner on Oct 1, 2021. It is now read-only.

Commit

Permalink
Merge branch 'custom-details' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
VChet committed Oct 13, 2020
2 parents 9af0285 + 599891e commit 724dce3
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 34 deletions.
27 changes: 22 additions & 5 deletions api/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const rateLimit = require("express-rate-limit");
const axios = require("axios");

const { CaptchaSecretKey } = require("../config");
const { Style } = require("../models/Style");

// Middleware
const router = express.Router();
Expand All @@ -12,6 +13,11 @@ const cache = apicache.middleware;
const onlyStatus200 = (req, res) => res.statusCode === 200;
const cacheSuccessful = cache("10 minutes", onlyStatus200);

const clearCache = (req, res, next) => {
apicache.clear();
next();
};

// Allow 1 request per 10 minutes
const GHRateLimiter = rateLimit({
windowMs: 10 * 60 * 1000,
Expand All @@ -35,16 +41,25 @@ const recaptcha = (req, res, next) => {
});
};

function isAdmin(req, res, next) {
const isAuthorized = async (req, res, next) => {
const { url } = req.body;
if (!url) return res.status(400).json({ error: "Request must contain url field" });
const existingStyle = await Style.findOne({ url }).lean();
if (!existingStyle) return res.status(404).json({ error: "Style does not exist" });
req.styleData = existingStyle;

if (process.env.NODE_ENV !== "production") return next();

if (!req.isAuthenticated()) {
return res.status(401).json({ error: "Authentication is required to perform this action" });
}
if (req.user.role !== "Admin") {
const isAdmin = req.user.role === "Admin";
const isOwner = req.user.username === existingStyle.owner;
if (!isAdmin && !isOwner) {
return res.status(403).json({ error: "You are not authorized to perform this action" });
}
next();
}
};

const {
getStyles,
Expand All @@ -53,6 +68,7 @@ const {
addStyle,
updateAllStyles,
updateStyle,
editStyle,
deleteStyle,
getStylesByOwner
} = require("./styles");
Expand All @@ -67,8 +83,9 @@ router.get("/search/:page?", searchStyle);
router.get("/owner/:owner/:page?", cacheSuccessful, getStylesByOwner);
router.post("/style/add", recaptcha, addStyle);
router.put("/style/update/all", GHRateLimiter, updateAllStyles);
router.put("/style/update/:id", GHRateLimiter, updateStyle);
router.delete("/style/delete", isAdmin, deleteStyle);
router.put("/style/update", GHRateLimiter, updateStyle);
router.put("/style/edit", isAuthorized, clearCache, editStyle);
router.delete("/style/delete", isAuthorized, clearCache, deleteStyle);

router.get("/me", getCurrentUser);

Expand Down
50 changes: 40 additions & 10 deletions api/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ function addStyle(req, res) {
}

function updateStyle(req, res) {
const styleId = req.params.id;
if (!styleId) return res.status(400).json({ error: "Request must contain styleId field" });
const { url } = req.body;
if (!url) return res.status(400).json({ error: "Request must contain url field" });

Style.findById(styleId, async (error, style) => {
Style.findOne({ url }, async (error, style) => {
if (error) return res.status(500).json({ error });
if (!style) return res.status(404).json({ error: "Style was not found in our base" });

Expand All @@ -163,8 +163,8 @@ function updateStyle(req, res) {
if (data.isArchived) return res.status(400).json({ error: "Repository is archived" });
if (data.isFork) return res.status(400).json({ error: "Repository is forked" });

Style.findByIdAndUpdate(
styleId,
Style.findOneAndUpdate(
{ url },
data,
{ new: true },
(updateError, newStyle) => {
Expand Down Expand Up @@ -193,12 +193,41 @@ function updateAllStyles(req, res) {
});
}

async function deleteStyle(req, res) {
const { url } = req.body;
const existingStyle = await Style.findOne({ url }).lean();
if (!existingStyle) return res.status(404).json({ error: "Style does not exist" });
function editStyle(req, res) {
const { url, ...customData } = req.body;
if (!customData.customName && !customData.customPreview) {
return res.status(400).json({ error: "Request must contain customName or customPreview fields" });
}

if (customData.customPreview) {
try {
const previewURL = new URL(customData.customPreview);
if (!previewURL.protocol.includes("https:")) {
return res.status(400).json({ error: "Preview must be from a secure source" });
}
} catch (error) {
return res.status(400).json({ error: "Invalid preview URL" });
}
}

// Remove non-custom fields
Object.keys(customData).forEach(key => {
const fieldToDelete = !["customName", "customPreview"].includes(key);
return fieldToDelete && delete customData[key];
});

Style.findOneAndDelete({ url }, (error, style) => {
Style.findOneAndUpdate(
{ url: req.styleData.url },
{ $set: customData },
{ new: true },
(error, style) => {
if (error) return res.status(500).json({ error });
return res.status(200).json({ style });
});
}

async function deleteStyle(req, res) {
Style.findOneAndDelete({ url: req.styleData.url }, (error, style) => {
if (error) return res.status(500).json({ error });
return res.status(200).json({ style });
});
Expand Down Expand Up @@ -230,6 +259,7 @@ module.exports = {
addStyle,
updateStyle,
updateAllStyles,
editStyle,
deleteStyle,
getStylesByOwner
};
4 changes: 1 addition & 3 deletions common.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ function addExpressMiddleware(app) {
"img-src": [
"'self'",
"data:",
"https://raw.githubusercontent.com",
"https://github.githubassets.com",
"https://www.google-analytics.com"
"https:"
],
"style-src": ["'self'", "'unsafe-inline'"],
"script-src": [
Expand Down
4 changes: 3 additions & 1 deletion models/Style.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ const schema = new Schema({
isPrivate: Boolean,
isArchived: Boolean,
isFork: Boolean,
customName: String,
customPreview: String
});

schema.index({ name: "text", owner: "text" });
schema.index({ name: "text", customName: "text", owner: "text" });
schema.plugin(mongoosePaginate);

exports.Style = mongoose.model("Style", schema);
2 changes: 1 addition & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ addExpressMiddleware(app);
app.use(express.static("public"));
app.use("/api", api);

app.get("/login", passport.authenticate("github", { scope: ["user:email"] }));
app.get("/login", passport.authenticate("github", { scope: ["read:user"] }));
app.get("/github/callback", passport.authenticate("github"), (req, res) => res.redirect("/"));

const clientIndex = path.join(__dirname, "public/index.html");
Expand Down
23 changes: 20 additions & 3 deletions src/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div id="app">
<app-header @open-nav-link="openNavLink" />
<Home />
<app-header :user="user" @open-nav-link="openNavLink" />
<Home :user="user" />
<app-footer @open-nav-link="openNavLink" />

<how-to-use-dialog :open="showHowtoUseModal" @close="showHowtoUseModal = false" />
Expand All @@ -11,6 +11,8 @@
</template>

<script>
import axios from 'axios';
import AppHeader from '@/components/AppHeader.vue';
import Home from '@/views/Home.vue';
import AppFooter from '@/components/AppFooter.vue';
Expand All @@ -32,7 +34,9 @@ export default {
return {
showHowtoUseModal: false,
showAddStyleModal: false,
showPrivacyModal: false
showPrivacyModal: false,
user: {}
};
},
computed: {
Expand All @@ -46,10 +50,23 @@ export default {
isActive ? $body.classList.add('no-scroll') : $body.classList.remove('no-scroll');
}
},
beforeMount() {
this.getUser();
},
mounted() {
this.$gtag.pageview({ page_path: window.location.pathname });
},
methods: {
getUser() {
axios
.get('/api/me')
.then((response) => {
if (!response.data.error) this.user = response.data.user;
})
.catch((error) => {
console.error(error);
});
},
openNavLink(navLink) {
this[navLink] = true;
}
Expand Down
23 changes: 17 additions & 6 deletions src/src/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<nav>
<button class="link" type="button" @click="$emit('open-nav-link', 'showHowtoUseModal')">How to Use</button>
<button class="link" type="button" @click="$emit('open-nav-link', 'showAddStyleModal')">Add Style</button>
<a v-if="!user.username" href="/login">Login</a>
<button v-else class="link" type="button">{{ user.username }}</button>
</nav>
</div>
</header>
Expand All @@ -17,6 +19,13 @@
<script>
export default {
name: 'AppHeader',
props: {
user: {
type: Object,
required: true,
default: () => {}
}
},
data() {
return {
menuIsActive: false
Expand Down Expand Up @@ -70,7 +79,7 @@ header {
width: 2rem;
height: 2rem;
@include media-size-mobile {
@include media-size-tablet {
display: inline-block;
}
Expand Down Expand Up @@ -100,11 +109,12 @@ header {
}
nav {
button {
button,
a {
margin-left: 1.5rem;
}
@include media-size-mobile {
@include media-size-tablet {
display: flex;
flex-basis: 100%;
flex-direction: column;
Expand All @@ -113,7 +123,8 @@ header {
animation: slide-in 0.5s forwards;
overflow: hidden;
button {
button,
a {
margin-top: 0.75rem;
margin-left: 0rem;
}
Expand Down Expand Up @@ -141,12 +152,12 @@ header {
max-height: 0;
}
to {
max-height: 5rem;
max-height: 10rem;
}
}
@keyframes slide-in {
from {
max-height: 5rem;
max-height: 10rem;
}
to {
max-height: 0;
Expand Down
13 changes: 11 additions & 2 deletions src/src/components/StyleCard.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
<template>
<div class="style-card">
<div class="image-container">
<img v-if="styleData.preview" :src="styleData.preview" :alt="`Preview of ${styleData.name} style`" />
<img
v-if="styleData.customPreview"
:src="styleData.customPreview"
:alt="`Preview of ${styleData.customName || styleData.name} style`"
/>
<img
v-else-if="styleData.preview"
:src="styleData.preview"
:alt="`Preview of ${styleData.customName || styleData.name} style`"
/>
<img v-else class="no-image" src="@/images/no-image.png" alt="No preview" />
<a
class="button style-button-filled"
Expand All @@ -15,7 +24,7 @@
</div>

<div class="data" @click="$emit('open', $props.styleData)">
<div class="name">{{ removeDashes(styleData.name) }}</div>
<div class="name">{{ styleData.customName || removeDashes(styleData.name) }}</div>
<div>by {{ styleData.owner }}</div>
<div class="footer">
<span>{{ pluralize(styleData.stargazers, 'star') }}</span>
Expand Down
19 changes: 18 additions & 1 deletion src/src/components/dialogs/PrivacyPolicyDialog.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<template>
<base-dialog v-if="open" size="large" @close="$emit('close')">
<template>
<div class="dialog-title">Privacy Policy</div>
<h1 class="dialog-title">Privacy Policy</h1>

<h2>Privacy Policy for StyleBase</h2>
<p>
This Privacy Policy document contains types of information that is collected and recorded by StyleBase,
accessible from https://stylebase.cc, and how we use it.
Expand All @@ -16,13 +17,27 @@
to the information that they shared and/or collect in StyleBase. This policy is not applicable to any
information collected offline or via channels other than this website.
</p>

<h2>Information we collect</h2>
<p>
The personal information that you are asked to provide, and the reasons why you are asked to provide it, will be
made clear to you at the point we ask you to provide your personal information.
</p>
<p>
If you contact us, we may receive additional information about you such as your name, email address, the
contents of the message and/or attachments you may send us, and any other information you may choose to provide.
</p>

<h2>Log Files</h2>
<p>
StyleBase follows a standard procedure of using log files. These files log visitors when they visit websites.
The information collected by log files include internet protocol (IP) addresses, browser type, Internet Service
Provider (ISP), date and time stamp, referring/exit pages, and possibly the number of clicks. These are not
linked to any information that is personally identifiable. The purpose of the information is for analyzing
trends, administering the site and gathering demographic information.
</p>

<h2>Cookies</h2>
<p>
Like any other website, StyleBase uses 'cookies'. These cookies are used to store information including the
pages on the website that the visitor accessed or visited. The information is used to optimize the users'
Expand All @@ -32,6 +47,8 @@
For more general information on cookies, please read the "What Are Cookies" article on
<a href="https://www.cookieconsent.com/what-are-cookies/">Cookie Consent website</a>.
</p>

<h2>Consent</h2>
<p>By using our website, you hereby consent to our Privacy Policy.</p>

<div class="dialog-buttons">
Expand Down

0 comments on commit 724dce3

Please sign in to comment.