github λ‘κ·ΈμΈ - λ‘κ·ΈμΈ κ·μΉ μ€μ
λ‘κ·Έμμ
join / login / github login μ²μλΆν° λ€μ ꡬνν΄λ³΄κΈ°
userκ° β passwordλ₯Ό κ°μ§κ±°λ β‘ githubμ emailμ΄ primary & verified λ κ²μ΄λΌλ©΄ λ‘κ·ΈμΈν μ μλλ‘ νλ €κ³ νλ€.
β μ μμμ ꡬνμ μλ£νκ³ , β‘λ₯Ό ꡬνν΄μΌ νλ€.
νμ¬ githubλ‘λΆν° λ°μ userμ emailλ€ κ°μ΄λ° primary & verified λ emailμ μ°Ύμλμ μνμ΄λ€.
μ΄μ μ΄ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userκ° μ μ githubλ‘ λ‘κ·ΈμΈνλ passwordλ‘ κ³μ μ μμ±νλ μκ΄μμ΄ λ‘κ·ΈμΈμμΌμ€ κ²μ΄λ€.
export const finishGithubLogin = (req, res) = {
// finalUrlμ λ§λ¦
// finalUrlλ‘λΆν° λ°μ΄ν°λ₯Ό κ°μ Έμ΄ (finalUrlμ POST requestλ₯Ό 보λ)
// κ·Έ λ°μ΄ν°λ₯Ό json νμμΌλ‘ λ°κΏ
// access tokenμ μ΄μ©ν΄ github APIμ μ κ·Όν΄ userμ λν μ 보λ₯Ό κ°μ Έμ΄
if ("access_token" in tokenRequest) {
const { access_token } = tokenRequest;
const apiUrl = "https://api.github.com";
// public λ°μ΄ν°
const userData = await (
await fetch(`${apiUrl}/user`, {
headers: {
Authorization: `token ${access_token}`,
},
})
).json();
// private λ°μ΄ν° (δΈ email)
const emailData = await (
await fetch(`${apiUrl}/user/emails`, {
headers: {
Authorization: `token ${access_token}`,
},
})
).json();
// primary & verified email κ°μ Έμ€κΈ°
const emailObj = emailData.find(email => email.primary === true && email.verified === true);
if (!emailObj) {
return res.redirect("/login");
}
// μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userλ₯Ό login μν¨λ€
const existingUser = await User.findOne({ email: emailObj.email });
if (existingUser) {
req.session.loggedIn = true;
req.session.user = existingUser;
return res.redirect("/");
} else {
// π₯ μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userμ κ³μ μ μμ±νλ€ (join)
}
} else {
return res.redirect("/login");
}
};
μ΄μ (νμ¬ μ°κ²°ν github κ³μ μ emailκ³Ό κ°μ emailλ‘ Join νμ¬ κ·Έ emailμ΄ DBμ μ μ₯λμ΄ μμ΄μΌ ν¨) login νμ΄μ§μμ 'Continue with Github β'λ₯Ό ν΄λ¦νλ©΄, λ‘κ·ΈμΈμ΄ λμ΄ homeμΌλ‘ redirect λλ©΄μ Log outκ³Ό profile λ©λ΄κ° λ¬λ€.
μ μ½λμμ π₯ λΆλΆμ ꡬννλ €κ³ νλ€.
μΌλ¨ Users DBμμ Join λμ΄ μλ user μ 보λ₯Ό μμ νλ€.
db.users.remove({})
User.js νμΌμμ userSchemaμ socialOnly κ°μ μΆκ°νλ€.
github λ‘κ·ΈμΈμ ν΅ν joinμΈμ§ μλμ§ μκΈ° μν΄ μ¬μ©λλ€.
λν, passwordμ λΆμ¬ν΄μ€ required: trueλ₯Ό μμ νλ€.
github λ‘κ·ΈμΈμ ν΅ν΄ join νλ©΄ passwordλ μ±μΈ μ μκΈ° λλ¬Έμ΄λ€.
(λ¨, join.pug νμΌμμ password inputμ κ·Έλλ‘ requiredμ¬μΌ νλ€. userSchemaμ λΉκ΅ν κ².)
// User.js
const userSchema = new mongoose.Schema({
// μλ΅
password: String,
socialOnly: { type: Boolean, default: false },
});
finishGithubLogin 컨νΈλ‘€λ¬λ₯Ό μμ νλ€.
githubλ‘λΆν° κ°μ Έμ¨ emailμ΄ DBμ μμ λ μμμ κ°μ Έμ¨ userDataμ eamilObjλ₯Ό μ΄μ©ν΄ κ³μ μ μμ±(Join)νλλ‘ νλ€.
export const finishGithubLogin = (req, res) = {
// finalUrlμ λ§λ¦
// finalUrlλ‘λΆν° λ°μ΄ν°λ₯Ό κ°μ Έμ΄ (finalUrlμ POST requestλ₯Ό 보λ)
// κ·Έ λ°μ΄ν°λ₯Ό json νμμΌλ‘ λ°κΏ
// access tokenμ μ΄μ©ν΄ github APIμ μ κ·Όν΄ userμ λν μ 보λ₯Ό κ°μ Έμ΄
// public λ°μ΄ν°
// private λ°μ΄ν° (δΈ email)
// primary & verified email κ°μ Έμ€κΈ°
// μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userλ₯Ό login μν¨λ€
const existingUser = await User.findOne({ email: emailObj.email });
if (existingUser) {
req.session.loggedIn = true;
req.session.user = existingUser;
return res.redirect("/");
} else {
// π₯ μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userμ κ³μ μ μμ±νλ€ (join)
const user = await User.create({
name: userData.name,
email: emailObj.email,
username: userData.login,
password: "", // githubμμ κ°μ Έμ¨ λ°μ΄ν°λ‘λΆν° password κ°μ μ±μΈ μ μλ€
socialOnly: true, // λμ githubλ₯Ό ν΅ν κ³μ μμ±μ΄λ κ²μ μ리기 μν΄ socialOnly κ°μ trueλ‘ λ°κΎΌλ€
location: userData.location,
});
}
} else {
return res.redirect("/login");
}
};
μ΄μ (νμ¬ μ°κ²°ν github κ³μ μ emailκ³Ό κ°μ emailμ΄ DBμ μ μ₯λμ΄ μμ§ μλλΌλ) login νμ΄μ§μμ 'Continue with Github β'λ₯Ό ν΄λ¦νλ©΄, λ‘κ·ΈμΈμ΄ λμ΄ homeμΌλ‘ redirect λλ©΄μ Log outκ³Ό profile λ©λ΄κ° λ¬λ€.
ννΈ, sicialOnly κ°μ΄ trueμΈ userλ login formμ ν΅ν΄ λ‘κ·ΈμΈν μ μλλ‘ postLogin 컨νΈλ‘€λ¬λ₯Ό μμ ν΄μΌ νλ€.
export const postLogin = async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, socialOnly: false }); // μμ β
// μ€λ΅
};
existingUser λΆλΆμμ μ€λ³΅λ μ½λλ₯Ό μμ νλ€. (existingUser β user)
userSchemaμ avatarUrlμ μΆκ°ν ν finishGithubLogin 컨νΈλ‘€λ¬μμ github emailμ μ΄μ©ν΄ κ³μ (user)μ μλ‘κ² μμ±νλ λΆλΆμ μ½λλ₯Ό μμ νλ€.
(passwordλ‘ κ³μ μ μμ±ν κ²½μ° avatarλ₯Ό κ°μ§μ§ μλλ€. λ€λ§, λμ€μ νλ‘νμμ μΆκ° κ°λ₯νλ€.)
export const finishGithubLogin = async (req, res) => {
// 1. finalUrlμ λ§λ¦
const baseUrl = "https://github.com/login/oauth/access_token";
const config = {
client_id: process.env.GH_CLIENT,
client_secret: process.env.GH_SECRET,
code: req.query.code,
};
const params = new URLSearchParams(config).toString();
const finalUrl = `${baseUrl}?${params}`;
// 2. finalUrlλ‘λΆν° λ°μ΄ν°λ₯Ό κ°μ Έμ΄ (finalUrlμ POST requestλ₯Ό 보λ) (code β access token)
// κ·Έλ¦¬κ³ κ·Έ λ°μ΄ν°λ₯Ό json νμμΌλ‘ λ°κΏ
const tokenRequest = await (
await fetch(finalUrl, {
method: "POST",
headers: {
Accept: "application/json",
},
})
).json();
// 3-1. access tokenμ μ΄μ©ν΄ github APIμ μ κ·Όν΄ userμ λν μ 보λ₯Ό κ°μ Έμ΄
if ("access_token" in tokenRequest) {
const { access_token } = tokenRequest;
const apiUrl = "https://api.github.com";
// 3-1-1. public λ°μ΄ν°
const userData = await (
await fetch(`${apiUrl}/user`, {
headers: {
Authorization: `token ${access_token}`,
},
})
).json();
// 3-1-2. private λ°μ΄ν° (δΈ email)
const emailData = await (
await fetch(`${apiUrl}/user/emails`, {
headers: {
Authorization: `token ${access_token}`,
},
})
).json();
// (1) primary & verified email κ°μ Έμ€κΈ°
const emailObj = emailData.find(
(email) => email.primary === true && email.verified === true
);
if (!emailObj) {
return res.redirect("/login");
}
// (2) μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userλ₯Ό login μν¨λ€
// (3) μ°Ύμ emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ μλ€λ©΄, κ·Έ userμ κ³μ μ μμ±νλ€ (join)
let user = await User.findOne({ email: emailObj.email });
if (!user) {
user = await User.create({
avatarUrl: userData.avatar_url,
name: userData.name,
email: emailObj.email,
username: userData.login,
password: "", // githubμμ κ°μ Έμ¨ λ°μ΄ν°λ‘λΆν° passwordλ μ±μΈ μ μλ€
socialOnly: true, // λμ , socialOnly κ°μ trueλ‘ λ°κΏμ€¬λ€
location: userData.location,
});
}
req.session.loggedIn = true;
req.session.user = user;
return res.redirect("/");
} else {
// 3-2. access_tokenμ΄ tokenRequest μμ μλ€λ©΄ userλ₯Ό login νμ΄μ§λ‘ 보λΈλ€.
return res.redirect("/login");
}
};
βββ μ΄μ , github emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ
μλ€λ©΄
, κ·Έ userκ° μ μ githubλ‘ λ‘κ·ΈμΈνλ passwordλ‘ κ³μ μ μμ±νλ μκ΄μμ΄, λ‘κ·ΈμΈμμΌμ€ κ²μ΄λ€.githubλ‘ λ‘κ·ΈμΈ
μ΄λ―Έ wetubeμμ githubλ₯Ό ν΅ν΄ κ³μ μ λ§λ μνμμ cookieλ₯Ό μ§μ λ‘κ·Έμμ νλ€.'Continue with Github β'λ₯Ό ν΄λ¦νλ©΄ wetubeμ λ‘κ·ΈμΈμ΄ λλ κ²μ νμΈν μ μλ€.
μ΄λ socialOnly κ°μ true μ΄λ€.passwordλ‘ κ³μ μμ±
μ΄λ²μλ cookieλ₯Ό μ§μ λ‘κ·Έμμ νκ³ , Users DBμμ githubλ₯Ό ν΅ν΄ μμ±ν κ³μ (user)μ μμ ν ν, Join νμ΄μ§μμ passwordμ ν¨κ»μμ wetubeλ₯Ό μΉμΈν github emailκ³Ό λμΌν email
μ μ λ ₯ν΄ κ³μ μ μμ±νλ€. (join)(wetubeλ₯Ό μΉμΈν κ³μ μΌλ‘ gtihub μ체μ λ‘κ·ΈμΈμ΄ λμ΄ μλ μνμμ) 'Continue with Github β'λ₯Ό ν΄λ¦νλ©΄ (μμ User DBμμλ githubλ₯Ό ν΅ν΄ μμ±ν κ³μ μ μμ νμμ§λΌλ, join νμ΄μ§μμ κ³μ μ λ§λ€ λ μ λ ₯ν emailμ΄ νμ¬ λ‘κ·ΈμΈ λμ΄ μλ github emailκ³Ό μΌμΉνλ―λ‘) wetubeμ λ‘κ·ΈμΈμ΄ λλ κ²μ νμΈν μ μλ€. π₯
β΅ const user = await User.findOne({ email: emailObj.email });
μ΄λ socialOnly κ°μ falseμ΄λ€.βββ ννΈ, github emailκ³Ό κ°μ emailμ κ°μ§ userκ° DBμ
μλ€λ©΄
, μμμ κ°μ Έμ¨ userDataμ eamilObjλ₯Ό μ΄μ©ν΄ κ³μ μ μμ±(Join)ν ν, λ‘κ·ΈμΈμμΌμ€ κ²μ΄λ€.
β» μμ
λ‘κ·ΈμΈ κ΅¬ν μ νΈμν°, μΉ΄μΉ΄μ€ν‘, μΈμ€νκ·Έλ¨ λ±μ client_idκ° app_id λ±μΌλ‘ λ°λμ΄ ννλ μλ μμ§λ§ μ 체μ μΌλ‘ githubμ κ±°μ λΉμ·ν κ³Όμ μ κ±°μΉλ€.
λ€λ§, ꡬκΈμ΄λ νμ΄μ€λΆ λ±μ μ’ λ μ μ°¨κ° λ³΅μ‘ν μ μλ€.
// userController.js
export const logout = (req, res) => {
req.session.destroy();
return res.redirect("/");
};
Log out λ©λ΄λ₯Ό ν΄λ¦νλ©΄, λ‘κ·Έμμλλ€.
λ©λ΄κ° Log out & Profileμμ Join & LoginμΌλ‘ λ°λλ€.
β» νΉμ λ²μ μ μ»€λ° κ°μ Έμ€κΈ°
$ git clone [λ νμ§ν 리 url]
$ git reset --hard [μνλ λ²μ 컀λ°μ ν΄μμ½λ]
git cloneμ μ΄μ©ν΄ user authentication ννΈ μ²μλΆν° λ€μ ꡬνν΄λ³΄κΈ°
ν·κ°λ Έκ±°λ λ€μ λ³΄κ³ μΆμ λΆλΆλ€λ§ μ 리ν¨
(join) λΉλ°λ²νΈ ν΄μ±
(login) μ
λ ₯ λΉλ°λ²νΈμ DB λΉλ°λ²νΈλ₯Ό λΉκ΅
expressμμ sessionμ μ¬μ©ν μ μλλ‘ νλ€.
λΈλΌμ°μ κ° μλ²μ μμ²μ 보λ΄λ©΄, μλ²λ λΈλΌμ°μ μκ² session idλ₯Ό μ£Όκ³ , session storeμ μΈμ idμ ν¨κ» μΈμ objectλ₯Ό μ μ₯ν΄μ€λ€.
λΈλΌμ°μ λ λ€μλΆν° μλ²μ requestλ₯Ό λ³΄λΌ λ κ·Έ session idλ₯Ό ν¨κ» 보λΈλ€.
κ°μ μΉ μ¬μ΄νΈμ κ°μ userλΌλ μλ‘ λ€λ₯Έ λΈλΌμ°μ μλ μλ‘ λ€λ₯Έ session idκ° λΆμ¬λλ―λ‘ μλ²λ μ΄λ₯Ό ν΅ν΄ λΈλΌμ°μ (user)λ₯Ό ꡬλΆν μ μκ³ , session storeμ userμ λν μ 보λ₯Ό μ λ°μ΄νΈν μ μλ€.
session λ°μ΄ν°λ₯Ό mongoDBμ μ μ₯νλλ‘ ν¨
// server.js
import MongoStore from "connect-mongo";
app.use(session({
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.DB_URL }),
})
);
process.envλ₯Ό ν΅ν΄ .env νμΌμ μ μν΄λμ κ²λ€μ μ κ·Όνκ³ μ¬μ©ν μ μλλ‘ ν¨
node.jsμμ fetch APIλ₯Ό μ¬μ©νκΈ° μν΄ μ€μΉν΄μΌ νλ€.
λ²μ 3λΆν°λ μλ¬κ° λ° μ μλ€.
ν΄κ²° λ°©λ²μ μμ§λ§ 곡μ λ¬Έμμμ CSM(CommonJS)λ₯Ό μ΄λ€λ©΄ λ²μ 2λ₯Ό κΆμ₯νκ³ μλ€. (λ€μ μ 리)
postJoin 컨νΈλ‘€λ¬μμ userκ° μ λ ₯ν emailμ΄ DBμ μλ emailκ³Ό μ€λ³΅λλ©΄, μλ¬ λ©μμ§λ₯Ό λμ°λλ‘ ν¨
postLogin 컨νΈλ‘€λ¬μμ github emailκ³Ό λμΌν emailμ΄ DBμ μμΌλ©΄, κ·Έ emailμ λΉλ‘―ν΄ githubλ‘λΆν° κ°μ Έμ¨ user μ 보λ₯Ό λ°νμΌλ‘ κ³μ μ μμ±νλλ‘ ν¨
β λ κ²½μ°κ° μ κΉ ν·κ°λ Έλ€. λ°°μΉλμ§ μλλ€!
db.users.remove({}) ν νμ join νλλ° μκΎΈ 'ν΄λΉ username/emailμ μ΄λ―Έ μ¬μ© μ€μ
λλ€.'λΌκ³ λμλ€.
μλ¬κ° λ°μν μμΈμ User.exists()λ₯Ό User.find()λΌκ³ μλͺ» μ μκΈ° λλ¬Έμ΄μλ€.
model.find()
λ 쑰건μ λ§μ‘±νλ λ°μ΄ν°λ₯Ό λ΄μ λ°°μ΄μ return νλ€.
μ¦, 쑰건μ λ§μ‘±νλ λ°μ΄ν°κ° μλ€λ©΄ κ·Έ κ°μ [ ](λΉ λ°°μ΄)μ΄ λλ€.
κ·Έλ°λ°, λΉ λ°°μ΄μ trueμ΄λ€! ( λΉ λ¬Έμμ΄μ falseμ΄μ§λ§, λΉ λ°°μ΄ & λΉ κ°μ²΄λ trueμ΄λ€. )
ννΈ, model.exists()
λ 쑰건μ λ§μ‘±νλ λ°μ΄ν°μ μ 무λ₯Ό Boolean κ°μΌλ‘ return νλ€.
μ¦, 쑰건μ λ§μ‘±νλ λ°μ΄ν°κ° μλ€λ©΄ κ·Έ κ°μ falseκ° λλ€.
μλ¬ μ½λ
export const postJoin = async (req, res) => {
const { name, email, username, password, password2, location } = req.body;
const exists = await User.find({ $or: [{ username }, { email }] });
console.log(exists); // [] β true
if (exists) {
return res.status(400).render("join", {
pageTitle: "Join",
errorMessage: "ν΄λΉ username λλ emailμ μ΄λ―Έ μ¬μ© μ€μ
λλ€.",
});
}
// μλ΅
}
μμ λ° ν΄κ²°
export const postJoin = async (req, res) => {
const { name, email, username, password, password2, location } = req.body;
const exists = await User.exists({ $or: [{ username }, { email }] });
console.log(exists); // false
if (exists) {
return res.status(400).render("join", {
pageTitle: "Join",
errorMessage: "ν΄λΉ username λλ emailμ μ΄λ―Έ μ¬μ© μ€μ
λλ€.",
});
}
// μλ΅
}
userSchemaλ₯Ό μ μν λ usernameκ³Ό emailμ 'unique: true'λ₯Ό μ μ΄μ€μΌ νλ€.
model.create() μ¬μ© μ try ~ catch ꡬ문μ μ¬μ©ν΄ μλ¬κ° λ°μν λλ₯Ό λλΉν΄μΌ νλ€.
μ½λ μμΌλ‘λ postLogin 컨νΈλ‘€λ¬μμ req.sessionμ κ°μ μ€ νμ middlewares.js νμΌ μμ localMiddlewareκ° μ€νλμ΄μΌ νκ³ , μ€μ λ‘λ κ·Έλ κ² μ€νλμ΄ μλ¬κ° λ°μνμ§ μκ³ μλ€.
κ·Έλ°λ° μλ next()κ° μλ middlewareλ controllerλ³΄λ€ μ μ μ€νλμ΄μΌ νλ κ±Έλ‘ μκ³ μλλ° μ΄κ² μ΄λ»κ² λ κ±΄μ§ λͺ¨λ₯΄κ² λ€.
// userController.js
export const postLogin = async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.status(400).render("login", {
pageTitle: "Login",
errorMessage: "ν΄λΉ usernameμ κ°μ§ κ³μ μ΄ μμ΅λλ€.",
});
}
const ok = <await bcrypt.compare(password, user.password);
if (!ok) {
return res.status(400).render("login", {
pageTitle: "Login",
errorMessage: "λΉλ°λ²νΈκ° ν립λλ€.",
});
}
req.session.loggedIn = true;
req.session.user = user;
return res.redirect("/");
};
// middlewares.js
export const localsMiddleware = (req, res, next) => {
res.locals.loggedIn = req.session.loggedIn;
res.locals.loggedInUser = req.session.user;
res.locals.siteName = "Wetube";
next();
};
// server.js
app.use(localsMiddleware);
app.use("/", rootRouter);
μ΄μ λ₯Ό μμΈν μ€λͺ
νμλ©΄, μ€ν μμλ μλμ κ°λ€.
login λ²νΌμ λλ₯΄λ©΄, expressκ° server.js νμΌμμ home routeλ₯Ό μ°Ύμ(β΅ /login) postLogin 컨νΈλ‘€λ¬κ° μ€νλκΈ° μ μ, κ·Έ μμ μ ν localsMiddlewareκ° λ¨Όμ μ€νλλ€.
λ€μμΌλ‘ postLogin 컨νΈλ‘€λ¬κ° μ μ€νλλ€κ° λ§μ§λ§ λΆλΆμμ res.sessionμ κ°μ μ€ ν res.redirect("/")μ μν΄ expressλ λ€μ home routeλ₯Ό μ°Ύλλ€.(β΅ /)
μ΄λ²μλ home 컨νΈλ‘€λ¬κ° μ€νλκΈ° μ μ, κ·Έ μμ μ ν localsMiddlewareκ° λ¨Όμ μ€νλλ€.
λ°λΌμ, κ²°κ³Όμ μΌλ‘ login λ²νΌμ λλ₯΄λ©΄ localsMiddleware β postLogin 컨νΈλ‘€λ¬ β localsMiddleware β home 컨νΈλ‘€λ¬
μμλλ‘ μ€νμ΄ λ¨μ μ μ μλ€.
node-fetchλ₯Ό import νμ λ°μν μλ¬μ΄λ€.
node-fetchκ° λ²μ 3λΆν°λ ESM-only Moduleμ΄λΌκ³ νλ€.
곡μ λ¬Έμμμλ CSM(CommonJS)λ₯Ό μ΄λ€λ©΄ λ²μ 2λ‘ μΈ κ²μ κΆμ₯νκ³ μλ€.
node uninstall node-fetch
npm install node-fetch@2.6.1
#{λ³μ} / =λ³μ
pug νμΌμμ μλ°μ€ν¬λ¦½νΈ λ³μλ₯Ό μΈ μ μμ
env
.env νμΌμ μ μν΄λμ κ°λ€μ process.envλ₯Ό μ΄μ©ν΄ μ¬μ© κ°λ₯
res.locals
pugμ expressκ° res.locals κ°μ 곡μ
μ μ λ³μμ²λΌ λͺ¨λ pug νμΌμμ μ¬μ© κ°λ₯